From Vertex AI to Gemini Enterprise Agent Platform: Build a Travel Intent Parser with Express Mode (Part 1)

Prototypes are easy. Production boundaries are not.

Picture this: your executive team wants an AI travel concierge for our platform, Offvia. A customer types:

“Find me a four-day tech-conference trip to Tokyo from San Francisco for under $2,200, departing next Wednesday. I need a non-stop flight, a boutique hotel near Shibuya with high-speed Wi-Fi, and free cancellation in case my visa decision is delayed.”

You open your cloud console to build a prototype. But before you can send a single prompt, enterprise reality hits: you need a new GCP project, billing account approvals, IAM security clearance, and private VPC routing.

Estimated turnaround: three weeks.

By the time the security review clears, business momentum is dead.

Google Cloud recognized this enterprise bottleneck. Alongside evolving Vertex AI into Gemini Enterprise Agent Platform, Google introduced Express Mode—a zero-friction sandbox that compresses weeks of bureaucratic onboarding into a 30-second start.

In this first guide of our 6-part series, we will bypass the red tape, dissect Google Cloud’s agentic shift, and build a bulletproof, schema-constrained Python intent parser for Offvia.

📌 Official Terminology: Google Cloud officially designates this architecture as Gemini Enterprise Agent Platform (formerly Vertex AI). In this series, we explore its Express Mode sandbox alongside Agent Studio and the Agent Development Kit (ADK), backed by foundational Vertex AI capabilities and Gemini Enterprise.


✈️ The Mental Model: Fast-Track Gate vs Customs Clearance

To understand where Express Mode fits, picture an international airport:

flowchart TD subgraph Express["🏃 1. Express Mode (Fast-Track Crew Gate)"] direction TB E1["⚡ 30-Second Instant Sandbox Project (No Billing Setup)"] E2["🔑 1-Click API Key Authentication (x-goog-api-key)"] E3["🌐 Public Internet Endpoints & Safety Quotas"] E4["🎯 Primary Objective: Rapid Prototyping in an Afternoon"] E1 --> E2 --> E3 --> E4 end Express ==>|"Graduate to Production When Ready"| Enterprise subgraph Enterprise["🛂 2. Gemini Enterprise (Full Customs Clearance)"] direction TB F1["🏛️ Standard GCP Organization & Resource Hierarchy"] F2["🛡️ Workload Identity, ADC & Least-Privilege IAM Roles"] F3["🔒 VPC Service Controls, Private Service Connect & CMEK"] F4["🚀 Primary Objective: Production SLAs, Compliance & Audit Logs"] F1 --> F2 --> F3 --> F4 end

When an airline crew lands for a 45-minute turnaround, they don’t stand in a two-hour general customs line. They flash their badge at the crew gate and walk straight onto the tarmac.

Conversely, permanent immigration requires rigorous visa checks and background audits.

  • Express Mode is your Fast-Track Gate: An ephemeral sandbox for testing prompts, validating data schemas, and proving business value before lunch.
  • Gemini Enterprise is your Customs Clearance: The governed runtime you graduate to before touching live credit cards, private customer PII, or internal VPC networks.

🏛️ The Paradigm Shift: Model Endpoints vs Cognitive Loops

For four years, Vertex AI operated on a model-centric paradigm: you sent tokens in, you received static tokens out. The model had no memory, no tools, and no operational awareness.

Modern Gemini Enterprise Agent Platform applications shift the entire center of gravity to autonomous cognitive loops:

flowchart TD subgraph Traditional["1. TRADITIONAL VERTEX AI (Model-Centric Paradigm)"] direction LR P1["User Prompt"] --> M1["[ Model Endpoint ]"] --> R1["Static Text / JSON Response"] end Traditional ==>|"Evolves into Autonomous Cognitive Loop"| Agentic subgraph Agentic["2. GEMINI ENTERPRISE AGENT PLATFORM (Agent-Centric Paradigm)"] direction TB User["User Goal: 'Plan Tokyo Trip'"] --> Engine subgraph Engine["🤖 Autonomous Agent Cognitive Loop"] direction TB LLM["🧭 Reasoning & Planning Engine
(Gemini 2.5 Flash)"] ADK["🛠️ Orchestration, Sessions & Memory
(Google ADK / Agent Studio)"] MCP["🔌 Remote Enterprise Toolsets (MCP)
• Offvia BigQuery Lakehouse • Live Airline GDS APIs"] LLM -->|State Loop| ADK ADK -->|Tool Invocation| MCP MCP -->|Tool Execution Results| ADK ADK -->|Refined Working Memory| LLM end Engine --> Output["✅ Validated Data Contract, Flight Options, or Clarification Turn"] end

The platform unifies four critical primitives:

  1. Agent Studio: Visual playground to craft prompts, wire models, and export code.
  2. Agent Development Kit (ADK): Open-source framework in Python and TypeScript for multi-agent state loops.
  3. Agent Garden: Enterprise blueprints for common workflows (RAG, financial advisory, support).
  4. Gemini Enterprise Agent Platform MCP: Native Model Context Protocol (MCP) integration for connecting private lakehouses and external APIs securely.

🔍 Four Practitioner Questions Answered

Before writing code, let’s clear up the four most common enterprise misconceptions:

  1. Can Express Mode run real Python code?
    Yes. It provisions a real GCP project. Your API key works immediately with the official google-genai SDK from your local laptop, Docker container, or virtualenv.
  2. Is an API key acceptable for production?
    No. An API key authenticates the project for billing and rate limits, but it carries zero user identity. Production services must use Application Default Credentials (ADC) or Workload Identity with least-privilege IAM roles (roles/aiplatform.user).
  3. Does structured JSON guarantee factual travel data?
    No. A schema enforces syntax, not truth. Pydantic ensures budget_limit is a number, but cannot stop an LLM from inventing a non-existent airport or a fantasy $40 flight. Real-world validation belongs in downstream tools.
  4. Can an Express Mode key access private BigQuery tables?
    No. Google Cloud services enforce IAM boundaries. A model API key cannot be used as a bearer token to read private enterprise lakehouses.

🧪 Hands-On Lab: Build Offvia’s Travel-Intent Parser

Most AI agent tutorials jump straight into autonomous tool loops while ignoring the unglamorous foundation: data contracts. If your agent ingests unformatted human prose and emits unpredictable JSON, your downstream booking APIs will crash on call one.

Here is the architectural rule: Separate model extraction from domain reference resolution.

If a traveler says “Tokyo”, Tokyo has two major international hubs: Haneda (HND) and Narita (NRT). Do not ask the LLM to guess which one. Extract the city, flag it for resolution, and let your downstream flight tool expand it deterministically.

Step 1: Environment Setup

mkdir -p ~/offvia-agent-platform && cd ~/offvia-agent-platform
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade google-genai pydantic tzdata

Retrieve your API key from the Express Mode console and set your shell environment:

export GEMINI_API_KEY="your-express-mode-api-key"
export GEMINI_MODEL="gemini-2.5-flash"

Step 2: Write offvia_intent_parser.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Offvia Travel Concierge -- Schema-Constrained Intent Parser (Part 1)
Bootstrapped on Google Cloud Gemini Enterprise Agent Platform Express Mode.
"""

from __future__ import annotations

import os
import sys
import logging
from datetime import date, datetime
from time import perf_counter
from uuid import uuid4
from zoneinfo import ZoneInfo
from typing import List, Optional

from google import genai
from google.genai import types
from pydantic import BaseModel, Field, ValidationError, field_validator

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("offvia.intent_parser")

MODEL_ID = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
REFERENCE_TIMEZONE = os.getenv("OFFVIA_TIMEZONE", "Europe/Rome")


class HotelCriteria(BaseModel):
    preferred_area: Optional[str] = Field(default=None, description="Neighborhood, e.g. Shibuya")
    min_star_rating: Optional[float] = Field(default=None, ge=1.0, le=5.0)
    required_amenities: List[str] = Field(default_factory=list)
    free_cancellation_required: bool = Field(default=False)


class TripIntent(BaseModel):
    # Separate raw traveler inputs from authoritative domain resolution
    origin_city: str = Field(description="Origin city, e.g. San Francisco")
    destination_city: str = Field(description="Destination city, e.g. Tokyo")
    supplied_origin_iata: Optional[str] = Field(
        default=None, 
        description="Populate ONLY if the traveler explicitly specified a 3-letter IATA code (e.g. SFO)"
    )
    airport_resolution_required: bool = Field(
        default=True, 
        description="True when downstream tools must resolve city to airport codes (e.g. Tokyo -> HND/NRT)"
    )
    
    trip_duration_days: int = Field(ge=1, le=60)
    
    # Preserve original wording for auditing alongside normalized ISO date
    departure_date_expression: Optional[str] = Field(default=None, description="Raw expression, e.g. 'next Wednesday'")
    departure_date: Optional[str] = Field(default=None, description="Resolved date in YYYY-MM-DD format")
    
    budget_limit: float = Field(gt=0)
    budget_currency: str = Field(description="3-letter currency code, e.g. USD")
    non_stop_required: bool = Field(default=False)
    
    hotel_preferences: HotelCriteria
    special_constraints: List[str] = Field(default_factory=list)
    unresolved_questions: List[str] = Field(default_factory=list)

    @field_validator("budget_currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        code = v.strip().upper()
        if len(code) != 3 or not code.isalpha():
            raise ValueError("budget_currency must be a 3-letter code")
        return code

    @field_validator("departure_date")
    @classmethod
    def validate_date(cls, v: Optional[str]) -> Optional[str]:
        if v:
            date.fromisoformat(v)
        return v


def build_client() -> genai.Client:
    api_key = os.getenv("GEMINI_API_KEY")
    if not api_key:
        raise RuntimeError("GEMINI_API_KEY not set. Export your key first.")

    return genai.Client(
        vertexai=True,
        api_key=api_key,
        http_options=types.HttpOptions(
            retry_options=types.HttpRetryOptions(attempts=4, initial_delay=1.0, max_delay=8.0)
        ),
    )


def parse_traveler_request(client: genai.Client, request_text: str, ref_date: date, tz: str) -> TripIntent:
    system_instruction = (
        "You are the intent ingestion component for Offvia travel platform. "
        "Extract only facts stated by the traveler. If the user states a city rather than an airport code, "
        "leave supplied_origin_iata as null and set airport_resolution_required to true. "
        "Capture raw date phrasing in departure_date_expression and normalized date in departure_date."
    )
    prompt = f"Reference Date: {ref_date.isoformat()}\nTimezone: {tz}\n\nQuery:\n{request_text}"

    response = client.models.generate_content(
        model=MODEL_ID,
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            response_mime_type="application/json",
            response_schema=TripIntent,
        ),
    )
    return TripIntent.model_validate_json(response.text)


if __name__ == "__main__":
    req_id = str(uuid4())[:8]
    sample_request = (
        "Find me a four-day tech-conference trip to Tokyo from San Francisco for no more than "
        "$2,200, departing next Wednesday. I need a non-stop flight, a four-star hotel near "
        "Shibuya with dedicated high-speed Wi-Fi, and free cancellation in case my visa decision is delayed."
    )

    ref_date = datetime.now(ZoneInfo(REFERENCE_TIMEZONE)).date()
    logger.info("Processing request [%s] via model [%s]", req_id, MODEL_ID)

    client = build_client()
    start = perf_counter()

    try:
        intent = parse_traveler_request(client, sample_request, ref_date, REFERENCE_TIMEZONE)
        elapsed_ms = (perf_counter() - start) * 1000
        logger.info("Request [%s] parsed in %.0f ms", req_id, elapsed_ms)
        print("\n=== Offvia Parsed Travel Contract ===")
        print(intent.model_dump_json(indent=2))
    except ValidationError as e:
        logger.error("Schema validation failed: %s", e)
        sys.exit(1)
    finally:
        client.close()

Step 3: Run and Inspect Output

python3 offvia_intent_parser.py

Output:

{
  "origin_city": "San Francisco",
  "destination_city": "Tokyo",
  "supplied_origin_iata": null,
  "airport_resolution_required": true,
  "trip_duration_days": 4,
  "departure_date_expression": "next Wednesday",
  "departure_date": "2026-09-30",
  "budget_limit": 2200.0,
  "budget_currency": "USD",
  "non_stop_required": true,
  "hotel_preferences": {
    "preferred_area": "Shibuya",
    "min_star_rating": 4.0,
    "required_amenities": [
      "dedicated high-speed Wi-Fi"
    ],
    "free_cancellation_required": true
  },
  "special_constraints": [
    "Japanese visa decision might be delayed"
  ],
  "unresolved_questions": []
}

Notice what happened:

  • Zero Hallucination: Pydantic guarantees valid types. The output cannot be broken JSON.
  • Date Traceability: Both "next Wednesday" and "2026-09-30" are preserved for debugging.
  • Clean System Boundaries: The parser extracted the traveler’s intent; it made no false claims about flight availability or live pricing.

🛡️ Production Roadmap: What Comes Next?

Extracting a validated TripIntent object is step one. In a production architecture, the lifecycle expands into five governed stages:

flowchart TD S1["🎯 Stage 1: Intent Ingestion
Pydantic V2 Contract Validation"] S2["✈️ Stage 2: Domain Verification
IATA Codes, Currency & GDS Airline Rules"] S3["🤖 Stage 3: Multi-Agent Specialist Team
Supervisor Orchestration & State Machine"] S4["🔌 Stage 4: Remote MCP Toolsets
BigQuery Lakehouse & Real-Time APIs"] S5["👤 Stage 5: Human-in-the-Loop Governance
Explicit User Approval & Booking Execution"] S1 --> S2 --> S3 --> S4 --> S5
  1. Domain Lookup: Downstream services resolve “Tokyo” to HND and NRT via authoritative database queries—never via LLM memory.
  2. Quota Resiliency: Express Mode has lower rate limits. Production systems combine SDK exponential backoff with Redis token-bucket rate limiting.
  3. Secret Security: Graduate to Google Cloud Workload Identity Federation so static API keys never touch production source code.
  4. Human in the Loop: Agents propose bookings; they never charge corporate credit cards without an explicit human confirmation gate.

🚀 What’s Next in Part 2?

In Part 2: Visual Prototyping in Agent Studio — Designing the Offvia Concierge, we will:

  • Recreate our intent instructions visually inside Agent Studio.
  • Compare prompt variations and inspect the code generated by the studio canvas.
  • Configure built-in tools and evaluate token consumption across complex multi-city itineraries.
  • Bridge the visual studio draft directly into Google ADK (google-adk) Python code.

📖 Official Documentation & Verified Standards

Reader Feedback

Did you find this article valuable?

Tap a reaction to let us know. Instant, private, and brews better content!

Share this guide:

Community Discussion 0