Hotel Agent
by rvjh
README.md
# LangGraph + FastMCP Multi-Agent Travel Assistant
An agent-to-agent (A2A-style) demo built using **LangGraph**, **FastMCP**, and **OpenAI**. The system coordinates two independent agent processes: a client-facing Travel Agent that parses user requests, and a specialized Hotel Agent exposed over HTTP via the Model Context Protocol (MCP).
---
## Architecture Overview
```text
USER
| (Natural Language)
v
+------------------+
| Travel Agent |
| (LangGraph) |
+------------------+
|
| MCP over HTTP (Streamable HTTP)
v
+------------------+
| FastMCP |
| (MCP Server) |
+------------------+
|
v
+------------------+
| Hotel Agent |
| (LangGraph) |
+------------------+
|
v
Hotel Database
```
* **Process 1 (Hotel Agent + FastMCP):** Runs an MCP server on `http://127.0.0.1:8001` exposing the `find_hotels` tool powered by a LangGraph node.
* **Process 2 (Travel Agent):** Parses natural language inputs, extracts search parameters using `gpt-4o-mini`, calls the remote Hotel Agent over MCP, and returns a formatted recommendation.
---
## Directory Structure
```text
a2a-langgraph-demo/
│
├── .env
├── requirements.txt
├── README.md
│
├── hotel_agent/
│ ├── __init__.py
│ ├── graph.py
│ └── server.py
│
└── travel_agent/
├── __init__.py
├── graph.py
└── main.py
```
---
## Prerequisites & Installation
### 1. Set Up Virtual Environment
```bash
# Windows
python -m venv myenv1
.\myenv1\Scripts\activate
# macOS / Linux
python3 -m venv myenv1
source myenv1/bin/activate
```
### 2. Install Dependencies
Create `requirements.txt`:
```text
langgraph
langchain
langchain-openai
fastmcp
python-dotenv
```
Install via pip:
```bash
pip install -U -r requirements.txt
```
### 3. Configure Environment Variables
Create `.env` in the root directory:
```env
OPENAI_API_KEY=your_openai_api_key_here
```
---
## Code Implementation
### `hotel_agent/graph.py`
```python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# ---------------------------------------------------------
# State
# ---------------------------------------------------------
class HotelState(TypedDict):
location: str
max_price: int
nights: int
hotels: list
# ---------------------------------------------------------
# Fake hotel database
# ---------------------------------------------------------
HOTELS = [
{
"name": "Bengaluru Grand Hotel",
"location": "Bengaluru",
"price": 2500,
"rating": 4.3,
},
{
"name": "MG Road Business Hotel",
"location": "Bengaluru",
"price": 3200,
"rating": 4.5,
},
{
"name": "Koramangala Comfort Inn",
"location": "Bengaluru",
"price": 1800,
"rating": 4.1,
},
{
"name": "Indiranagar Premium Hotel",
"location": "Bengaluru",
"price": 4500,
"rating": 4.7,
},
{
"name": "Mumbai Central Hotel",
"location": "Mumbai",
"price": 4000,
"rating": 4.2,
},
]
# ---------------------------------------------------------
# LangGraph node
# ---------------------------------------------------------
def search_hotels(state: HotelState):
location = state["location"].lower()
max_price = state["max_price"]
matching_hotels = []
for hotel in HOTELS:
if (
hotel["location"].lower() == location
and hotel["price"] <= max_price
):
matching_hotels.append(hotel)
# Highest rating first
matching_hotels.sort(
key=lambda x: x["rating"],
reverse=True,
)
return {
"hotels": matching_hotels
}
# ---------------------------------------------------------
# Build LangGraph
# ---------------------------------------------------------
builder = StateGraph(HotelState)
builder.add_node("search_hotels", search_hotels)
builder.add_edge(START, "search_hotels")
builder.add_edge("search_hotels", END)
# Expose compiled graph variable imported by server.py
hotel_graph = builder.compile()
```
---
### `hotel_agent/server.py`
```python
from fastmcp import FastMCP
from .graph import hotel_graph
# ---------------------------------------------------------
# Create MCP server
# ---------------------------------------------------------
mcp = FastMCP("Hotel Agent")
# ---------------------------------------------------------
# MCP Tool
# ---------------------------------------------------------
@mcp.tool
def find_hotels(
location: str,
max_price: int,
nights: int,
) -> dict:
"""
Find hotels for a location and maximum price per night.
"""
print()
print("===================================")
print("HOTEL AGENT")
print("===================================")
print(f"Location : {location}")
print(f"Max price: ₹{max_price}")
print(f"Nights : {nights}")
# -----------------------------------------
# Call LangGraph
# -----------------------------------------
result = hotel_graph.invoke(
{
"location": location,
"max_price": max_price,
"nights": nights,
"hotels": [],
}
)
print()
print("Hotels found:", len(result["hotels"]))
return {
"location": location,
"max_price": max_price,
"nights": nights,
"hotels": result["hotels"],
}
# ---------------------------------------------------------
# Start server
# ---------------------------------------------------------
if __name__ == "__main__":
print()
print("===================================")
print("HOTEL AGENT MCP SERVER")
print("===================================")
print("Starting on http://127.0.0.1:8001")
print()
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8001,
)
```
---
### `travel_agent/graph.py`
```python
import json
from typing import TypedDict, Any
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from fastmcp import Client
# ---------------------------------------------------------
# MCP server URL
# ---------------------------------------------------------
MCP_SERVER_URL = "http://127.0.0.1:8001/mcp"
# ---------------------------------------------------------
# State
# ---------------------------------------------------------
class TravelState(TypedDict):
user_request: str
hotel_request: dict
hotel_result: Any
final_answer: str
# ---------------------------------------------------------
# LLM
# ---------------------------------------------------------
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
)
# ---------------------------------------------------------
# Node 1: Understand user's request
# ---------------------------------------------------------
def understand_request(state: TravelState):
user_request = state["user_request"]
print()
print("===================================")
print("TRAVEL AGENT")
print("===================================")
print("User request:")
print(user_request)
prompt = f"""You are a travel assistant.
Extract hotel search information from the user's request.
User request:
{user_request}
Return ONLY valid JSON.
The JSON must have exactly these fields:
{{
"location": "city",
"max_price": 5000,
"nights": 2
}}"""
response = model.invoke(prompt)
content = response.content
content = content.replace("```json", "").replace("```", "").strip()
hotel_request = json.loads(content)
print()
print("Travel Agent understood:")
print(hotel_request)
return {"hotel_request": hotel_request}
# ---------------------------------------------------------
# Node 2: Call Hotel Agent through MCP
# ---------------------------------------------------------
async def call_hotel_agent(state: TravelState):
request = state["hotel_request"]
print()
print("===================================")
print("A2A-LIKE AGENT COMMUNICATION")
print("===================================")
print("Travel Agent -> Hotel Agent")
print()
print(request)
async with Client(MCP_SERVER_URL) as client:
result = await client.call_tool(
"find_hotels",
request,
)
print()
print("Hotel Agent -> Travel Agent")
print()
print(result)
return {"hotel_result": result}
# ---------------------------------------------------------
# Node 3: Generate final response
# ---------------------------------------------------------
def create_final_answer(state: TravelState):
request = state["hotel_request"]
hotel_result = state["hotel_result"]
prompt = f"""You are a travel assistant.
The user requested:
Location: {request["location"]}
Maximum price per night: ₹{request["max_price"]}
Number of nights: {request["nights"]}
The Hotel Agent returned:
{hotel_result}
Create a concise response for the user.
For every hotel include:
- Hotel name
- Price per night
- Rating
- Estimated total for the requested number of nights
Do not invent information."""
response = model.invoke(prompt)
return {"final_answer": response.content}
# ---------------------------------------------------------
# Build Travel LangGraph
# ---------------------------------------------------------
builder = StateGraph(TravelState)
builder.add_node("understand_request", understand_request)
builder.add_node("call_hotel_agent", call_hotel_agent)
builder.add_node("create_final_answer", create_final_answer)
builder.add_edge(START, "understand_request")
builder.add_edge("understand_request", "call_hotel_agent")
builder.add_edge("call_hotel_agent", "create_final_answer")
builder.add_edge("create_final_answer", END)
travel_graph = builder.compile()
```
---
### `travel_agent/main.py`
```python
import asyncio
import os
from dotenv import load_dotenv
from .graph import travel_graph
load_dotenv()
async def main():
user_request = """
Find me a hotel in Bengaluru for 2 nights.
My budget is 5000 rupees per night.
"""
print()
print("===================================")
print("USER")
print("===================================")
print(user_request)
result = await travel_graph.ainvoke(
{
"user_request": user_request,
"hotel_request": {},
"hotel_result": {},
"final_answer": "",
}
)
print()
print("===================================")
print("FINAL RESPONSE")
print("===================================")
print(result["final_answer"])
if __name__ == "__main__":
asyncio.run(main())
```
---
## Execution Guide
### 1. Test the Hotel Graph Standalone
Ensure compilation and import work without error:
```bash
python -c "from hotel_agent.graph import hotel_graph; print(hotel_graph.invoke({'location':'Bengaluru','max_price':5000,'nights':2,'hotels':[]}))"
```
### 2. Start the Hotel Agent MCP Server
In **Terminal 1**:
```bash
python -m hotel_agent.server
```
Leave this process running on `http://127.0.0.1:8001`.
### 3. Run the Travel Agent
In **Terminal 2**:
```bash
python -m travel_agent.main
```
---
## Architectural Note: MCP vs. A2A
* **Current Architecture (MCP as Tool Interface):**
`Travel Agent ──(MCP Tool Call)──> Hotel Agent Server ──> LangGraph`
FastMCP exposes the `find_hotels` function as a standard tool endpoint.
* **Target Multi-Agent Architecture (Peer-to-Peer A2A):**
`Travel Agent <──(A2A Protocol Negotiation)──> Hotel Agent <──(MCP)──> Internal DB / Tools`
In a formal A2A system, agents converse and delegate tasks via an agent protocol, while MCP remains the interface between each individual agent and its local or external tool suite.