AI Automation & Agents  

AI Agents in Practice: Corporate Travel Booking Agent

Introduction

Here’s a fourth, production-ready pattern you can drop into your stack: a Corporate Travel Booking Agent. It reads a traveler’s request, checks company travel policy and budget, searches itineraries, holds the best compliant option, and—only with approval or within self-serve limits—purchases the ticket. It never claims success unless the backend executed the action and returned a receipt.


The Use Case

Employees ask for flights (origin, destination, dates, cabin, constraints). The agent should enforce policy (max fare, allowed cabins by route, preferred carriers), handle approvals for over-policy cases, and return a single held or purchased itinerary with a confirmation reference and clear next steps.


Prompt Contract (agent interface)

# file: contracts/travel_booking_v1.yaml
role: "TravelBookingAgent"
scope: >
  Plan and book business travel within policy. Ask once for missing fields
  (user_id, origin, destination, depart_date, return_date, cabin).
  Prefer preferred carriers; enforce fare caps; propose tools and never assert
  success unless a receipt is present.
output:
  type: object
  required: [summary, decision, itinerary, citations, next_steps, tool_proposals]
  properties:
    summary: {type: string, maxWords: 70}
    decision: {type: string, enum: ["search_only","hold","purchase","need_approval","need_more_info"]}
    itinerary:
      type: object
      required: [origin, destination, depart, return, carrier, cabin, price_cents]
      properties:
        origin: {type: string}
        destination: {type: string}
        depart: {type: string}
        return: {type: string}
        carrier: {type: string}
        cabin: {type: string}
        price_cents: {type: integer}
    citations: {type: array, items: {type: string}}
    next_steps: {type: array, items: {type: string}, maxItems: 6}
    tool_proposals:
      type: array
      items:
        type: object
        required: [name, args, preconditions, idempotency_key]
        properties:
          name: {type: string, enum: [GetPolicy, SearchFlights, HoldItinerary, EnsureApproval, PurchaseTicket]}
          args: {type: object}
          preconditions: {type: string}
          idempotency_key: {type: string}
policy_id: "travel_policy.v6.us"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
  narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
  bullets:   {top_p: 0.82, temperature: 0.45}

Example claims (context given to the model)

[
  {"claim_id":"policy:farecap:domestic","text":"Domestic roundtrip fare cap is $700 all-in.",
   "effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"$700 all-in"},
  {"claim_id":"policy:cabin:domestic","text":"Domestic flights must be Economy; Premium Economy allowed if >4 hours.",
   "effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"Economy; Premium Economy >4h"},
  {"claim_id":"policy:preferred:carrier","text":"Preferred carriers are BlueSky and AeroLine.",
   "effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"Preferred carriers"},
  {"claim_id":"policy:approval:overcap","text":"Over-cap fares require manager approval.",
   "effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"require manager approval"}
]

Tool Interfaces (typed, with receipts)

# tools.py
from pydantic import BaseModel
from datetime import date, datetime, timedelta
from typing import Optional, Dict, List

class GetPolicyArgs(BaseModel):
    user_id: str
    route_type: str  # "domestic" | "international"

class SearchFlightsArgs(BaseModel):
    origin: str
    destination: str
    depart_date: date
    return_date: date
    cabin: str  # "economy" | "premium_economy" | "business"
    preferred_carriers: Optional[List[str]] = None
    max_results: int = 5

class HoldItineraryArgs(BaseModel):
    user_id: str
    carrier: str
    price_cents: int
    cabin: str
    origin: str
    destination: str
    depart_iso: str
    return_iso: str
    hold_minutes: int = 24*60

class EnsureApprovalArgs(BaseModel):
    user_id: str
    manager_id: str
    reason: str
    price_cents: int

class PurchaseTicketArgs(BaseModel):
    hold_ref: str
    user_id: str

class ToolReceipt(BaseModel):
    tool: str
    ok: bool
    ref: str
    message: str = ""
    data: Optional[Dict] = None
# adapters.py  (demo logic)
from tools import *
from datetime import date, datetime, timedelta

PREFERRED = ["BlueSky","AeroLine"]
FARE_CAP_DOMESTIC = 70000  # cents

def get_policy(a: GetPolicyArgs) -> ToolReceipt:
    return ToolReceipt(tool="GetPolicy", ok=True, ref=f"pol-{a.user_id}",
                       data={"fare_cap_cents": FARE_CAP_DOMESTIC if a.route_type=="domestic" else 300000,
                             "preferred": PREFERRED,
                             "cabin_rules": "economy; premium_economy if >4h domestic"})

def search_flights(a: SearchFlightsArgs) -> ToolReceipt:
    # mock 2 results; in real life call GDS/OTA/airline API
    results = [
      {"carrier":"BlueSky","price_cents": 58000,"cabin": "economy",
       "depart_iso": f"{a.depart_date}T09:10:00","return_iso": f"{a.return_date}T17:35:00",
       "origin": a.origin, "destination": a.destination},
      {"carrier":"AeroLine","price_cents": 74500,"cabin": "premium_economy",
       "depart_iso": f"{a.depart_date}T08:00:00","return_iso": f"{a.return_date}T18:20:00",
       "origin": a.origin, "destination": a.destination}
    ]
    return ToolReceipt(tool="SearchFlights", ok=True, ref="search-rt", data={"results": results[:a.max_results]})

def hold_itinerary(a: HoldItineraryArgs) -> ToolReceipt:
    expires_at = (datetime.utcnow() + timedelta(minutes=a.hold_minutes)).isoformat()
    return ToolReceipt(tool="HoldItinerary", ok=True, ref=f"hold-{a.carrier}-{a.origin}{a.destination}",
                       data={"expires_at": expires_at, "price_cents": a.price_cents})

def ensure_approval(a: EnsureApprovalArgs) -> ToolReceipt:
    # pretend manager approved; return false sometimes in real impl
    approved = a.price_cents <= 90000  # approve if ≤ $900 for demo
    return ToolReceipt(tool="EnsureApproval", ok=approved,
                       ref="appr-123" if approved else "appr-denied",
                       message="Approved" if approved else "Denied")

def purchase_ticket(a: PurchaseTicketArgs) -> ToolReceipt:
    return ToolReceipt(tool="PurchaseTicket", ok=True, ref=f"tkt-{a.hold_ref}",
                       data={"ticket_number": "880-1234567890"})

Agent Loop (proposal → verification → execution → receipts)

# agent_travel.py
import uuid, json
from datetime import date
from typing import Any, Dict, List
from tools import *
from adapters import *

ALLOWED_TOOLS = {"GetPolicy","SearchFlights","HoldItinerary","EnsureApproval","PurchaseTicket"}

def new_idem() -> str:
    return f"idem-{uuid.uuid4()}"

def verify_proposal(p: Dict[str, Any]) -> str:
    required = {"name","args","preconditions","idempotency_key"}
    if not required.issubset(p): return "Missing proposal fields"
    if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
    if p["name"] == "PurchaseTicket" and "hold_ref" not in p["args"]:
        return "Purchase requires hold_ref"
    return ""

def execute(p: Dict[str, Any]) -> ToolReceipt:
    n, a = p["name"], p["args"]
    if n == "GetPolicy":        return get_policy(GetPolicyArgs(**a))
    if n == "SearchFlights":
        a["depart_date"] = date.fromisoformat(a["depart_date"])
        a["return_date"] = date.fromisoformat(a["return_date"])
        return search_flights(SearchFlightsArgs(**a))
    if n == "HoldItinerary":    return hold_itinerary(HoldItineraryArgs(**a))
    if n == "EnsureApproval":   return ensure_approval(EnsureApprovalArgs(**a))
    if n == "PurchaseTicket":   return purchase_ticket(PurchaseTicketArgs(**a))
    return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")

# --- Model shim returning a plan per contract (replace with your LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], req: Dict[str,Any]) -> Dict[str,Any]:
    # pretend the model chose the preferred, in-cap itinerary
    itinerary = {
      "origin": req["origin"], "destination": req["destination"],
      "depart": f"{req['depart_date']}T09:10:00","return": f"{req['return_date']}T17:35:00",
      "carrier": "BlueSky", "cabin": "economy", "price_cents": 58000
    }
    decision = "purchase" if itinerary["price_cents"] <= 70000 else "need_approval"
    return {
      "summary": f"Found a preferred-carrier economy roundtrip within cap; proceeding to hold then purchase.",
      "decision": decision,
      "itinerary": itinerary,
      "citations": ["policy:preferred:carrier","policy:farecap:domestic","policy:cabin:domestic"],
      "next_steps": ["Fetch policy", "Search flights", "Hold itinerary", "Purchase if within cap or approved"],
      "tool_proposals": [
        {"name":"GetPolicy","args":{"user_id":req["user_id"],"route_type":"domestic"},
         "preconditions":"Load fare caps and preferred carriers.","idempotency_key": new_idem()},
        {"name":"SearchFlights",
         "args":{"origin":req["origin"],"destination":req["destination"],
                 "depart_date":req["depart_date"],"return_date":req["return_date"],
                 "cabin":req.get("cabin","economy"),"preferred_carriers":["BlueSky","AeroLine"],"max_results":5},
         "preconditions":"Find compliant options; prefer preferred carriers.","idempotency_key": new_idem()},
        {"name":"HoldItinerary",
         "args":{"user_id":req["user_id"],"carrier":"BlueSky","price_cents":58000,"cabin":"economy",
                 "origin":req["origin"],"destination":req["destination"],
                 "depart_iso":f"{req['depart_date']}T09:10:00","return_iso":f"{req['return_date']}T17:35:00",
                 "hold_minutes":1440},
         "preconditions":"Hold best compliant option.","idempotency_key": new_idem()},
        {"name":"EnsureApproval",
         "args":{"user_id":req["user_id"],"manager_id":req["manager_id"],"reason":"Travel booking over cap",
                 "price_cents":58000},
         "preconditions":"Approval required only if price exceeds cap.","idempotency_key": new_idem()},
        {"name":"PurchaseTicket",
         "args":{"hold_ref":"hold-BlueSky-"+req["origin"]+req["destination"],"user_id":req["user_id"]},
         "preconditions":"Only if within cap or approval ok.","idempotency_key": new_idem()}
      ]
    }

def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
    idx = {r.tool:r for r in receipts}
    lines = [model_json["summary"], ""]
    it = model_json["itinerary"]
    lines.append(f"Itinerary: {it['origin']} → {it['destination']} ({it['depart']} / {it['return']})")
    lines.append(f"Carrier {it['carrier']}, cabin {it['cabin']}, price ${it['price_cents']/100:.2f}")
    lines.append("")
    lines.append(f"Decision: {model_json['decision']}")
    lines.append("Next:")
    for s in model_json["next_steps"]: lines.append(f"• {s}")
    if idx.get("HoldItinerary") and idx["HoldItinerary"].ok:
        lines.append(f"\nHeld: {idx['HoldItinerary'].ref}, expires {idx['HoldItinerary'].data['expires_at']}")
    if idx.get("PurchaseTicket") and idx["PurchaseTicket"].ok:
        lines.append(f"Purchased: ticket {idx['PurchaseTicket'].data['ticket_number']} (ref {idx['PurchaseTicket'].ref})")
    lines.append("\nCitations: " + ", ".join(model_json["citations"]))
    return "\n".join(lines)

def handle(req: Dict[str,Any]) -> str:
    contract = open("contracts/travel_booking_v1.yaml").read()
    claims: List[Dict[str,Any]] = []  # load real policy claims per region/company
    plan = call_model(contract, claims, req)

    receipts: List[ToolReceipt] = []
    for prop in plan["tool_proposals"]:
        reason = verify_proposal(prop)
        if reason:
            receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
        rec = execute(prop)
        receipts.append(rec)
        if not rec.ok and prop["name"] in {"PurchaseTicket"}:
            break
    return render_response(plan, receipts)

if __name__ == "__main__":
    request = {
      "user_id":"U314",
      "manager_id":"M271",
      "origin":"SFO",
      "destination":"SEA",
      "depart_date":"2025-11-05",
      "return_date":"2025-11-07",
      "cabin":"economy"
    }
    print(handle(request))

The Prompt You’d Send to the Model (concise and testable)

System:
You are TravelBookingAgent. Follow the contract:
- Ask once if user_id, origin, destination, dates, or cabin are missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, decision, itinerary{}, citations[], next_steps[], tool_proposals[].

Claims (eligible only):
[ ... JSON array of travel policy claims like above ... ]

User:
Book me a round trip from SFO to SEA Nov 5–7, economy, user U314, manager M271.

How to adapt quickly

Wire search_flights, hold_itinerary, and purchase_ticket to your GDS/OTA or airline APIs; keep idempotency and holds before purchase. Load policy claims from your travel policy with fare caps, preferred carriers, and cabin rules by route type and region. Add a validator step before execution for schema, lexicon, locale, citation coverage, “no implied writes,” and budget/SLO adherence. Ship the contract, policy bundle, and decoder settings as a bundle behind a feature flag + canary + rollback.