Python FastAPI backend + Next.js frontend, joined by the AG-UI protocol and CopilotKit. The backend follows ports & adapters: the agent and domain models sit in the middle, and everything replaceable — prompts, flights, hotels, persistence — plugs in from the edges.
Each capability is a small interface (port) in backend/app/ports/ with one adapter
implementation in backend/app/adapters/. The agent only ever sees the port.
| Capability | Port | Adapter | Backed by |
|---|---|---|---|
| Prompts | PromptServicePort | FilePromptService | Markdown files on disk |
| Flights | FlightServicePort | KiwiMCPFlightServiceAdapter | Remote Kiwi.com MCP (streamable HTTP) |
| Accommodations | AccommodationServicePort | MCPAccommodationServiceAdapter | Local FastMCP stdio server → SerpApi Google Hotels |
| Research | ResearchServicePort | GroundedResearchService | research_agent.py sub-agent (Gemini grounding) |
| Trips | TripRepository | SqliteTripRepository | SQLAlchemy async + aiosqlite |
| Profile | UserProfileRepository | SqliteUserProfileRepository | Singleton row in SQLite |
| Saved scenarios | SavedScenarioRepository | SqliteSavedScenarioRepository | Scenario JSON + denormalized columns |
| Trip backgrounds | TripBackgroundRepository | SqliteTripBackgroundRepository | Image bytes + generation-lock status in SQLite |
Destination, duration, month, budget, travelers — plus home city, direct-flights-only, transit modes, hotel
class, vibe tags, currency. Preferences layer with merged_with(): global profile first, conversation on top.
Leg carries a TransportMode (flight / train / bus / ferry / car), times and fares.
DaySummary gives each day a title and a time-blocked schedule of PlanItems.
Each Scenario bundles an itinerary with a CostBreakdown (transport / stay / grand total)
and StressFactors (layovers, overnight travel, tight connections, total hours) → a 1–5 stress score
.
Live results incl. a booking_link deep link and a routing-label airline (Kiwi exposes no carrier name).
Date options power the cheapest-dates card.
Name, property type, nightly + total rate, currency, rating, reviews, hotel class, amenities, link,
coordinates, and an estimated flag when values are approximate.
Trip (with a full AG-UI message_history JSON column), Message,
UserProfile singleton, SavedScenario, and TripBackground (generated vibe
image + a pending/ready/error lock) — all on a shared SQLAlchemy metadata.
The main agent orchestrates; a research sub-agent handles Google-grounded search (Gemini won't reliably combine grounding with function tools and structured output in a single call), and a second, purely fire-and-forget sub-agent paints each trip's vibe background image.
| Tool | What it does | Data source | Fallback |
|---|---|---|---|
gather_preferences | Extracts and persists traveler preferences to the global profile | Conversation | — |
find_cheapest_dates | Samples a month with concurrent ±3-day flex searches, keeps the cheapest fare per departure date | Kiwi.com MCP | search_web |
search_flights | Live flight options; max_stops / time-of-day filters applied client-side | Kiwi.com MCP | search_web |
search_accommodations | Live hotels/rentals with nightly + total rates, honoring max_price / min_rating | SerpApi Google Hotels MCP | search_web |
search_web | Grounded destination research and the universal price fallback | Gemini Google grounding | — |
search_ground_transport | Train/bus/ferry routing: operators, times, duration, frequency, fare range | Research sub-agent | — |
generate_scenarios | Renders 2–3 validated scenarios; recomputes totals, enforces a complete day-by-day plan, supports targeted refinement by label | Agent-built | — |
ask_clarifying_question | Frontend human-in-the-loop card — pauses the run until the traveler answers | CopilotKit useHumanInTheLoop | — |
set_background_image | Only on an explicit request to change the vibe image — kicks off a forced regeneration | Image sub-agent | — |
available: false, and the agent falls
back to search_web. For flights specifically, a transport/protocol failure (dead connection,
timeout) raises KiwiTransportError in mcp_flight_service.py rather than silently
returning []; the search_flights/find_cheapest_dates tools catch it and
add a data_source_error: true flag, so a dead Kiwi connection stays distinguishable from Kiwi
genuinely finding zero itineraries for a route.Dynamic, but injects only the current date (day granularity) so the KV-cache prefix stays stable. Encodes seasonality reasoning, multi-modal sequencing with transfer buffers, round-trip-by-default, stress scoring heuristics, manual-override rules, and the scenario-comparison contract.
Langfuse v4 over OpenTelemetry: Agent.instrument_all() traces every run (LLM calls, tool
inputs/outputs) grouped per trip via session_id. Fully no-op when keys are absent.
image_agent.py mirrors the research sub-agent's isolation: a cheap scene_agent turns
the conversation + vibe tags into a destination + scene prompt, a Gemini image model renders it, and the
result is downscaled to WebP and stored — never on the main request path.
backend/evals/ runs the real agent + prompt headlessly against deterministic fixtures — 30
cases, 10 evaluators, an LLM-as-judge for soft criteria, and a live dashboard. Full reference:
evals.html / evals.md.
One row per conversation thread. Carries the full AG-UI history — user/assistant/tool turns incl. tool calls + results — so generative-UI cards rehydrate on reload.
Flattened plain-text turns (legacy/fallback), cascade-deleted with the trip.
Singleton of global preferences — loaded as the baseline for every run, editable in the settings modal.
Bookmarked scenarios: full Scenario JSON plus queryable columns
(destination, total, stress, dates). trip_id is SET NULL so saves outlive a deleted conversation.
One row per trip (cascade-deleted with it): generated image bytes + MIME type, the scene it came from, and a pending/ready/error status that doubles as a generation lock.
Desktop shows a 3-pane split — trip sidebar · chat · live map. Below lg the chat goes
full-width, the sidebar becomes an off-canvas drawer, and modals become bottom-sheets.
preferences · clarify (HITL) · cheapest-dates · flights · accommodations · research · ground-transport ·
scenario-comparison. Loading states show an animated candy search-progress pill; between tool calls a
pink status pill (chat-status.js) replaces the default loading dot.
Price + stress gauge, cost-breakdown bars, stress-factor tiles, a per-direction travel timeline with layover/overnight/tight chips and booking links, an expandable day-by-day itinerary, and Save / View-on-map actions.
Built on @vis.gl/react-google-maps: numbered stops, accommodation markers, mode-colored polylines
(geodesic for flights), auto-fit bounds. Client-side geocoding with a localStorage cache; degrades
to a friendly placeholder without a key.
trip-backdrop.js — a self-healing image that fades in only once the generated background loads,
and stays invisible on a 404. Reused behind the sidebar row and the scenario-detail modal header.
Domain, dependency injection, persistence,
agent, and REST routes via TestClient + dependency overrides against an isolated SQLite DB.
Frontend unit tests for the itinerary helpers.
End-to-end specs: API, persistence, saved scenarios, and settings flows.