Skip to main content

Tutorial 8 — Trip planner end-to-end

This capstone tutorial walks through the Adiona/muno trip-planner reference app. It connects the pieces built across the trip-planner rounds: the Muno React UI, the NoETL itinerary-planner playbook, Firestore event sourcing, Duffel test-environment flights and orders, Google Places enrichment, Amadeus hotels, and the Material widget renderer.

Everything in this tutorial is test environment only. Duffel orders use Duffel's test wallet and synthetic offers. Amadeus calls use the test API unless explicitly overridden elsewhere. The goal is to show the architecture and developer workflow, not to sell or ticket real travel.

Muno trip planner chat start

Vision

Muno is a chat-first trip-planner shell. A traveler can type free-form requests, submit structured widgets, and gradually build an itinerary. The frontend stays thin: it validates and renders widget envelopes, shows trip state, and forwards user turns. The NoETL itinerary agent does the orchestration work by appending events, dispatching MCP tools, and choosing the next widget to emit.

flowchart LR
User["Traveler"] --> Muno["Muno React UI"]
Muno --> Agent["NoETL itinerary-planner.yaml"]
Agent --> Firestore["mcp/firestore<br/>event log + trip docs"]
Agent --> Duffel["mcp/duffel<br/>test flights + test orders"]
Agent --> Places["mcp/google-places<br/>places + maps enrichment"]
Agent --> Amadeus["mcp/amadeus<br/>hotels + fallback travel data"]
Firestore --> Calendar["calendar_view widget"]
Duffel --> Widgets["flight_list + order_confirmation"]
Places --> Widgets
Amadeus --> Widgets
Widgets --> Muno

The important design point is that every integration is a playbook boundary. Muno does not know how to call Duffel, Google Places, Amadeus, or Firestore directly. It renders widget JSON and lets NoETL preserve the audit trail.

RoundDeliverableProof
1Duffel test order toolscreate_order execution 625452830463099795 created booking ref XKQAYC.
2Duffel Stays decisionStays was unavailable on the account, so hotels use Amadeus for v1.
3Firestore MCP event sourcingFirestore MCP v6 registered as catalog 625474366091821164; append_event seq smoke passed.
4Itinerary agentMuno PR #1 added playbooks/itinerary-planner.yaml and canonical widget examples.
5Calendar viewCalendar smoke execution 625590045860168070 rendered calendar_view and wrote a Firestore event.
6Muno UI + Material widgetsMuno PR #2 replaced JSON stubs with Material UI widgets and a theme.
7This tutorialScreenshots captured from the Muno UI and shipped into docs.

Prerequisites

You need a GKE NoETL environment with the trip-planner MCP playbooks registered. The working project used:

  • GCP project noetl-demo-19700101.
  • GKE cluster noetl-cluster in us-central1.
  • Worker service account [email protected] with Workload Identity enabled.
  • Firestore Native mode database (default) in us-central1.
  • MCP playbooks registered for mcp/firestore, mcp/duffel, mcp/google-places, and mcp/amadeus.
  • Duffel test token in Secret Manager as duffel-api-test.
  • Google Maps widget key in Secret Manager as google-maps-widget-key.
  • Muno cloned locally with dependencies installed.

Credential examples in this tutorial intentionally use placeholders:

export VITE_GOOGLE_MAPS_KEY="<restricted-widget-api-key>"
export VITE_NOETL_API_BASE_URL="http://localhost:8082/api"

For the Google Maps setup model, see the Pattern C reference in ai-meta: playbooks/google-maps-platform-setup-pattern-c.md.

For the lower-level travel-agent architecture that came before Muno, start with Tutorial 7 — Travel agent with widgets and the visual companion, Tutorial 8 — Travel agent GUI walkthrough.

Walkthrough

The screenshots below were captured from a clean local Muno dev session pointed at the GKE NoETL service through a port-forward:

kubectl -n noetl port-forward svc/noetl 8082:8082

cd /Volumes/X10/projects/noetl/ai-meta/repos/travel
VITE_NOETL_API_BASE_URL=http://localhost:8082/api \
VITE_GOOGLE_MAPS_KEY="<restricted-widget-api-key>" \
npm run dev -- --host 127.0.0.1 --port 5173

Open http://127.0.0.1:5173/.

1. Starting the chat

The initial Muno shell has three working areas:

  • The left rail shows guest mode and navigation for searches and orders.
  • The center thread holds the assistant turns and widgets.
  • The right pane mirrors current trip state.

The traveler starts with a simple free-form request: "Find flights to Paris." In the live agent, that user message is appended to Firestore and passed into the extraction step. The UI can also emit structured widget submissions later in the same thread.

Chat start and three-column shell

2. Free-form chat to place card

The extraction step converts the user's free-form text into slot updates and tool requests. For a destination phrase such as "Paris", the agent can dispatch Google Places through mcp/google-places.search_text.

The widget renderer then shows a place card. In the captured demo, the card is the Eiffel Tower with rating metadata, a landmark tag, and an Add to itinerary action.

Place card with Eiffel Tower

The Google Places enrichment round validated Pattern C auth on GKE: backend calls used Workload Identity OAuth, while widget image URLs used the restricted widget key. Direct Google Places MCP execution 625307783436435727 returned Eiffel Tower data. The round remained AMBER only where Amadeus test API 500s blocked unrelated hotels and locations paths.

3. Date-range picker

Once the agent knows the destination, it asks for dates with a date_range_picker widget. A widget submit becomes another user turn: the frontend emits the selected values, and the agent applies check_in_date and check_out_date slot updates.

Date range picker and early flights

In v1, this is still a developer demo path. The widget contract is the important part: the browser does not mutate hidden agent state. It emits an event, NoETL records it, and the next agent turn decides what to do.

Flights use Duffel by default. The search-only Duffel MCP round added automation/agents/mcp/duffel and changed the travel runtime default to flight_provider: duffel, while preserving Amadeus as an opt-out.

The default Duffel travel smoke was execution 625385246619337115. It rendered 10 Duffel offers with effective_flight_provider=duffel. The direct Duffel search_offers smoke 625385503302353446 also returned 10 capped offers.

Flight search results

The compact flight cards expose two actions:

  • Watch In Detail opens or renders a fuller offer view.
  • Book This advances to the test-order path.

5. Book a flight in the test environment

When the traveler clicks Book This, the agent can call mcp/duffel.create_order with a synthetic passenger and wallet-balance payment. This is still Duffel test mode: no real money, no real ticketing, and no production token.

The order-tools round validated this with execution 625452830463099795, which created test booking reference XKQAYC and order id ord_0000B6EeISs9tYodkIZlhY. The UI screenshot below shows the v1 confirmation widget shape.

Picked flight and booking confirmation

The displayed ABC123 reference is a synthetic placeholder in this captured widget. The real Duffel test reference is stored in the event and order audit path. Surfacing the exact provider booking reference in the order_confirmation card is one of the v1 polish items listed below.

6. Hotel search through Amadeus

Hotels use Amadeus in v1 because Duffel Stays was not enabled for the test account. The agent dispatches mcp/amadeus.search_hotels and renders hotel_list / hotel_card widgets.

Hotel search results

Amadeus test API reliability is tracked separately. The known friendly failure path is proven by execution 625309687340073612, where the Amadeus test API returned HTTP 500 before Google enrichment could run. That is not treated as a wiring regression: NoETL returns a handled friendly widget instead of crashing the trip flow.

7. Schedule populates through Firestore

After a flight order and hotel choice, the itinerary agent writes calendar event documents through mcp/firestore. The calendar_view widget can render static event payloads or subscribe to a Firestore path when Firebase web config is available.

Calendar view with schedule

The Firestore MCP round registered automation/agents/mcp/firestore as catalog 625474366091821164 and verified exactly six tools: set_doc, get_doc, query_collection, delete_doc, append_event, and replay_events. Its append_event smoke proved transactional sequence assignment with [1, 2, 3, 4, 5] and mandatory header redaction.

The calendar-view round then proved the Muno path end-to-end with execution 625590045860168070. That execution rendered calendar_view variant full and wrote a user_note event document under chat_threads/_smoke-calendar-1778643344/trip/current/events/.... The smoke documents were deleted after validation.

8. Itinerary summary and confirm

The final review step is an itinerary_summary widget. It gathers the destination, date range, selected travel items, rough cost, and confirmation actions into one card. The chat input remains available at the bottom, so the traveler can still say things like "add a 3pm museum visit Wednesday" and let the agent append another event.

Itinerary summary and input bar

Confirming the itinerary should be another event-sourced turn rather than a hidden browser-side mutation. That keeps replay possible: the same thread can be inspected later to see exactly which user turn, widget submit, tool call, and widget emission produced the final state.

Behind the scenes: replay the session

The replay helper lives in ai-meta as an operator script:

cd /Volumes/X10/projects/noetl/ai-meta

scripts/firestore_replay.sh thread-list --limit 5

Once you know the thread path, inspect the first few events:

scripts/firestore_replay.sh events chat_threads/<thread-id> --from 1 --to 30

A healthy session reads like a structured trace:

{
"events": [
{ "seq": 1, "type": "user_message", "text": "Find flights to Paris" },
{ "seq": 2, "type": "agent_slot_update", "slot_updates": { "region": "Paris" } },
{ "seq": 3, "type": "agent_tool_call", "tool": "google_places.search_text" },
{ "seq": 4, "type": "agent_tool_response", "status": "ok" },
{ "seq": 5, "type": "agent_widget_emit", "widget_type": "place_card" }
]
}

Full third-party request and response snapshots belong in events/api_calls/ with sensitive headers redacted. The Firestore MCP smoke explicitly verified that Authorization is stored as <REDACTED> while harmless headers such as Accept remain visible.

Replay is deterministic for slot updates, tool sequence, and emitted widget types. Natural-language assistant text can drift across LLM providers and model versions, so the stable contract is the event log and widget envelope, not exact prose.

What is not covered in v1

The capstone is intentionally honest about the first pass:

  • Real-money booking is not covered. Duffel live tokens, payment processor choice, KYC, refunds, and PCI scope are separate product and compliance threads. This tutorial uses Duffel test mode only.
  • Mobile responsive polish is deferred. The current screenshots are desktop-first. Figma has mobile variants, but they are not the scope of this walkthrough.
  • Multi-user authentication is not covered. The screenshots show Guest mode. Firebase Auth and per-user access hardening are future work.
  • Google Calendar sync is intentionally absent. Calendar data is Firestore-only in this scope. No Google Calendar API and no ICS export are included.
  • Place and hotel photos are placeholders. The widget schemas and Maps Static key path exist, but the captured card photos are grey placeholders.
  • Party display needs formatting polish. The itinerary summary shows Party: {"adults":2} instead of a human string such as 2 adults.
  • The booking reference in the screenshot is synthetic. Real Duffel test references such as XKQAYC are present in the event/order audit path, but the v1 card still displays a placeholder reference.
  • The captured dates are not fully consistent. The date picker shows 07/01/2026 to 07/05/2026, while the sample schedule shows Jul 15, 2026. That is a demo-data polish issue, not a widget contract issue.
  • The right pane is not fully surfaced. It shows destination and dates in the capture. The slot accumulator can grow to party, budget, star rating, amenities, and other search parameters.
  • Filter-narrowing widgets are not yet agent-emitted. The contract includes filters, but v1 does not yet drive star-rating sliders, budget ranges, or amenities checkboxes as a complete refinement loop.

Try it yourself

Start from the docs and source repos:

cd /Volumes/X10/projects/noetl/ai-meta
git submodule update --init --recursive

Run the GKE port-forward:

kubectl -n noetl port-forward svc/noetl 8082:8082

Start Muno locally:

cd repos/travel
npm install

VITE_NOETL_API_BASE_URL=http://localhost:8082/api \
VITE_GOOGLE_MAPS_KEY="<restricted-widget-api-key>" \
npm run dev -- --host 127.0.0.1 --port 5173

Open:

http://127.0.0.1:5173/

Then try a simple starting prompt:

Find flights to Paris for July 1 through July 5

Use the widget cards to pick dates, choose a flight, review hotels, and confirm the itinerary. Then use scripts/firestore_replay.sh from ai-meta to inspect the thread's event log.