AAmerica Cruise Line
← Back to the voyage desk

Behind the desk

How this page works.

The voyage desk is not a search box with a chat wrapper. It decides what to look up, looks it up across four separate systems, and assembles one answer — or tells you it cannot. Here is that process, first in plain terms and then in detail.

Every number on this site is invented.Sailings, prices, availability and policies were written to be plausible so the system could be built and demonstrated. Many details are certainly wrong. Nothing here should be used to plan or book a real holiday.

General Overview

In plain terms

Ask a question the way you would ask a person — “can four of us get a verandah room on the Treasure next August, and what would it cost?” — and the desk works out what it needs to know before it answers.

That question alone needs four separate lookups: which sailings exist, what is still unsold, what a party of four actually pays, and what deposit secures it. Nobody wrote a script for that particular question. The desk reads it, picks the tools it needs, and chains them, carrying the sailing it found in the first step into the three that follow.

Where answers come from

Nothing is answered from memory. Every price, cabin count and policy detail in a reply came from a lookup made while you waited, against one of four systems — the sailing inventory, the pricing engine, guest records, and a library of policy and shipboard documents.

Those systems are deliberately kept apart, and none of them can query another. Joining inventory to pricing to your booking history is the desk’s job, not the database’s, which is exactly how it would work on a real reservations platform.

What it will not do

The most useful thing the desk does is stop. Ask about something the documents do not cover — onboard casinos, mobile roaming charges — and it is built to say so rather than reach for the nearest loosely related page and improvise.

That sounds obvious and is surprisingly hard. A search system will always return something; its five best results for an unanswerable question are just its five least-bad ones. Recognising that they are all bad, and returning nothing at all, is a specific piece of engineering described in the next section.

Technical Description

In detail

The request path

A question travels from the browser to a Next.js route handler, which streams it through to a FastAPI service over Server-Sent Events. That service runs a Google ADK agent driving Gemini, which holds fourteen tools: eleven reached over the Model Context Protocol, and three in-process retrieval functions.

browser
  └─ POST /api/chat            Next.js route, streams SSE straight through
      └─ POST /chat/stream     FastAPI
          └─ ADK Runner        the model decides which tools to call, and when
              ├─ McpToolset ──────────► FastMCP server :8011
              │                            ├─ inventory  :5443
              │                            ├─ pricing    :5444
              │                            └─ guests     :5445
              └─ retrieval tools ───────► pgvector       :5442

Tool activity is streamed to the browser as it happens, which is why the panel names each lookup while the answer is still being written.

Agentic retrieval, not a retrieval pipeline

There is no unconditional “search, then answer” step. An availability question goes straight to SQL and never touches the document store; a dress-code question goes only to the documents; a “what should we do in Nassau and what will it cost” question does both and joins the results. The model chooses, and that choice is what makes this agentic rather than a pipeline.

The four databases

DatabaseHolds
inventoryShips, stateroom categories, sailings, day-by-day itineraries, live cabin counts
pricingNightly rate basis, seasonal multipliers, age and berth brackets, fees, add-ons, deposit rules
guestsProfiles, preferences, travelling parties, past reservations
voyage_vectors58 policy, excursion and ship-experience documents, stored as embeddings

Four separate Postgres instances, not four schemas in one. No tool can join across them, so composition has to happen in the agent — a constraint kept on purpose, because it is the constraint a production deployment would have.

Why the SQL sits behind a protocol

The three transactional databases are reached only through a FastMCP server speaking the Model Context Protocol. That indirection buys a lifecycle: the data plane can move to its own service without the agent changing. Document retrieval deliberately does not go through it, because retrieval is part of the model’s reasoning loop rather than a system of record.

Retrieval runs two searches at once

Document search combines a dense vector search with Postgres full-text search. They fail differently, which is the point: embeddings catch paraphrase, where a guest says “somewhere quiet and indulgent” and the document says “adults-exclusive”. Keyword search catches the exact rare term — a venue name, “passport” — that embeddings blur into its neighbours.

The two result sets merge by Reciprocal Rank Fusion, which combines rankings rather than scores and so needs no calibration between two systems whose numbers mean different things.

How it decides to return nothing

Each arm applies relevance floors before fusion, and getting these right was the substantial work.

  • Keyword floors are based on inverse document frequency: matching one rare word clears them, matching one ubiquitous word like “deck” does not. Two thresholds are OR’d, because an absolute score is unreachable for a one-word question while a coverage ratio is too harsh on a long one.
  • Distance alone cannot work. Measured on this corpus, the nearest document to an unanswerable question sits about as close as the right answer to a real one — the two distributions overlap almost entirely. Tightening a distance threshold trades recall for abstention roughly one for one, and never separates them.
  • The shape of the neighbourhood does work. When the corpus can answer a question, the best match stands out from the rest. When it cannot, every candidate is equally mediocre. Comparing the closest document against the fifth-closest more than doubled the abstention rate at no cost to the answers.

An optional last pass sends the surviving passages to a small model and asks whether each one actually answers the question, rather than merely being about the subject. That is the only thing that reliably rejects a question sitting one hop from a real document — roaming charges next to the wifi policy — because one hop away is, geometrically, very close.

Money is never the model’s arithmetic

Fares are not linear: a seasonal multiplier, then age brackets crossed with berth slots where the first two guests pay full fare, then per-guest fees and taxes. All of it is computed in exact decimal arithmetic with half-up rounding, and the pricing tool returns a fully itemised breakdown so the model has nothing left to calculate. The agent is instructed never to do fare maths itself.

A question, end to end

The real chain of calls

This is the actual sequence for “can 4 adults get a verandah room on the America Treasure next August, and what would it cost?” — each step feeding the next.

find_sailings              ship + an August date range   →  TRE-2027-08-12
list_stateroom_categories  which categories sleep four
check_stateroom_availability  what is left on that sailing  →  4 verandah rooms
quote_cruise_fare          the sailing, category and party →  itemised total
get_booking_rules          the fare  →  deposit and final payment date

Five lookups across two databases, one answer. The first call returns the sailing identifier that the following four all depend on, which is why the order matters and why a fixed script would not have served.

How well it works

Measured, not asserted

Retrieval is the part of a system like this that fails quietly — an answer reads just as fluently when the wrong passage was fetched. So quality is a number here: 96 labelled questions, 24 of which the documents genuinely cannot answer.

SearchRight firstFound at allNoiseSays “I don’t know”
Keyword only0.860.910.3925%
Meaning only0.930.960.3762%
Both, combined0.920.970.5221%

Combining both searches finds the right document 97% of the time. Abstention is the weakest column and the honest one: fusion returns something whenever either search does, so the keyword arm sets the ceiling. A regression gate fails the build if any of these slip.

Right now

Read live from the same health endpoint the desk uses, so this page cannot describe a system that is not running.

Checking…