> ## Documentation Index
> Fetch the complete documentation index at: https://genai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Stateful Travel Assistant Project

> Build a stateful Travel Assistant (Weather Advisory & Budget Planner) agent pipeline with memory checkpointers using LangGraph

In this page, we build a stateful, collaborative multi-agent **Travel Assistant** application using LangGraph. This assistant extracts a travel destination, runs a weather forecast search, and calculates a budget estimation, all while remembering user preferences across chat turns.

## 1. Core Concepts Explained

This project illustrates the following agentic concepts:

* **State Management**: Using the shared graph state to pass specialized variables (like `destination`, `weather_info`, and `budget_estimate`) between agents.
* **Separation of Duties**: Dividing tasks so the **Weather Node** focuses solely on weather details, and the **Budget Node** handles numerical calculations.
* **Persistent Session Thread**: Using a checkpointer to remember what destination the user is planning to visit, allowing them to ask follow-up questions without repeating details.

***

## 2. Key APIs & Classes Used

* **`StateGraph`**: The core graph builder class from LangGraph.
* **`MemorySaver`**: An in-memory database checkpointer used to store chat sessions.
* **`add_messages`**: Reducer function telling the graph to append new chat messages instead of overwriting the history.
* **`thread_id`**: Session key passed to the checkpointer to scope the memory to a specific conversation thread.

***

## 3. Step-by-Step Practical Implementation

### Step 1: Define the Shared State Schema

The state holds the conversation history (`messages`), the `destination` city, the retrieved `weather_info`, and the generated `budget_estimate`:

```python theme={null}
from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class TravelState(TypedDict):
    # Appends new messages to the list
    messages: Annotated[list[BaseMessage], add_messages]
    # Destination city extracted from user prompt
    destination: str
    # Weather description fetched by tool
    weather_info: str
    # Financial estimation calculated by agent
    budget_estimate: str
```

### Step 2: Create a Weather Search Tool

We write a mock tool using the `@tool` decorator that returns the current temperature and forecast for a destination:

```python theme={null}
from langchain_core.tools import tool

@tool
def get_city_weather(city: str) -> str:
    """Retrieves the current weather forecast and temperature for a given city."""
    forecasts = {
        "tokyo": "18°C, Cloudy with light rain.",
        "paris": "22°C, Sunny and clear skies.",
        "new york": "28°C, Hot and humid."
    }
    key = city.lower().strip()
    return forecasts.get(key, f"No weather records found for: '{city}'. Defaulting to 20°C, Mild.")
```

### Step 3: Define Node Functions

Nodes read from the state, call the LLM/tools, and return state key updates:

* **Weather Node**: Analyzes the query, calls the weather tool, and updates `destination` and `weather_info`.
* **Budget Node**: Calculates hotel/flight costs based on the destination and updates `budget_estimate`.

```python theme={null}
from langchain_core.messages import AIMessage, HumanMessage
from langchain.chat_models import init_chat_model

llm = init_chat_model("gemini-2.5-flash", model_provider="google_genai")

def weather_node(state: TravelState):
    """Weather Agent: Extracts destination, calls weather tool, and updates state."""
    print("\n[Node: Weather Agent] Checking forecast...")
    messages = state["messages"]
    
    # Bind tool to LLM
    llm_with_tools = llm.bind_tools([get_city_weather])
    response = llm_with_tools.invoke(messages)
    
    city = "paris"  # Default fallback
    weather = "22°C, Sunny"
    
    if response.tool_calls:
        tool_call = response.tool_calls[0]
        city = tool_call["args"].get("city", "paris")
        weather = get_city_weather.invoke(tool_call["args"])
        
    return {
        "messages": [AIMessage(content="I have fetched the weather updates.")],
        "destination": city,
        "weather_info": weather
    }

def budget_node(state: TravelState):
    """Budget Agent: Uses destination to calculate estimated costs."""
    print("\n[Node: Budget Agent] Calculating travel estimates...")
    city = state.get("destination", "paris")
    weather = state.get("weather_info", "")
    
    prompt = [
        HumanMessage(content=(
            f"You are a travel advisor. Calculate a basic 3-day travel budget estimate "
            f"(lodging + flight) for visiting {city}. State if the weather ({weather}) is good for travel."
        ))
    ]
    
    response = llm.invoke(prompt)
    return {
        "messages": [response],
        "budget_estimate": response.content
    }
```

### Step 4: Assemble Nodes and Edges

We construct the graph, adding nodes and setting the routing path:

```python theme={null}
from langgraph.graph import StateGraph, START, END

builder = StateGraph(TravelState)

# Register nodes
builder.add_node("weather_agent", weather_node)
builder.add_node("budget_agent", budget_node)

# Map edges
builder.add_edge(START, "weather_agent")
builder.add_edge("weather_agent", "budget_agent")
builder.add_edge("budget_agent", END)
```

### Step 5: Compile with Memory Saver

We compile the graph with an in-memory checkpointer:

```python theme={null}
from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()
travel_app = builder.compile(checkpointer=memory)
```

***

## 4. Combined Running Code Project

Here is the complete, self-contained Python script combining all steps:

```python theme={null}
import os
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_core.tools import tool
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver

# 1. Load keys
load_dotenv()

# 2. Define State Schema
class TravelState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    destination: str
    weather_info: str
    budget_estimate: str

# 3. Define Tool
@tool
def get_city_weather(city: str) -> str:
    """Retrieves the current weather forecast and temperature for a given city."""
    forecasts = {
        "tokyo": "18°C, Cloudy with light rain.",
        "paris": "22°C, Sunny and clear skies.",
        "new york": "28°C, Hot and humid."
    }
    key = city.lower().strip()
    return forecasts.get(key, f"No weather records found for: '{city}'. Defaulting to 20°C, Mild.")

# 4. Initialize LLM
llm = init_chat_model("gemini-2.5-flash", model_provider="google_genai")

# 5. Define Nodes
def weather_node(state: TravelState):
    print("\n[Node: Weather Agent] Checking forecast...")
    messages = state["messages"]
    llm_with_tools = llm.bind_tools([get_city_weather])
    response = llm_with_tools.invoke(messages)
    
    city = "paris"
    weather = "22°C, Sunny"
    
    if response.tool_calls:
        tool_call = response.tool_calls[0]
        city = tool_call["args"].get("city", "paris")
        weather = get_city_weather.invoke(tool_call["args"])
        print(f" -> Found city: '{city}'. Weather: '{weather}'")
        
    return {
        "messages": [AIMessage(content=f"I checked the weather forecast for {city}. It is: {weather}")],
        "destination": city,
        "weather_info": weather
    }

def budget_node(state: TravelState):
    print("\n[Node: Budget Agent] Calculating travel estimates...")
    city = state.get("destination", "paris")
    weather = state.get("weather_info", "")
    
    prompt = [
        HumanMessage(content=(
            f"You are a travel advisor. Calculate a basic 3-day travel budget estimate "
            f"(lodging + flight) for visiting {city}. State if the weather ({weather}) is good for travel."
        ))
    ]
    response = llm.invoke(prompt)
    print(" -> Calculations complete.")
    return {
        "messages": [response],
        "budget_estimate": response.content
    }

# 6. Assemble Graph
builder = StateGraph(TravelState)
builder.add_node("weather_agent", weather_node)
builder.add_node("budget_agent", budget_node)
builder.add_edge(START, "weather_agent")
builder.add_edge("weather_agent", "budget_agent")
builder.add_edge("budget_agent", END)

# 7. Compile with Memory
memory = MemorySaver()
travel_app = builder.compile(checkpointer=memory)

# 8. Run
if __name__ == "__main__":
    # Create thread session
    config = {"configurable": {"thread_id": "tokyo-trip-2026"}}
    
    print("=== Start Travel Session ===")
    
    # Turn 1: Ingest destination and run multi-agent checks
    query1 = "Plan a 3-day trip to Tokyo."
    print(f"\nUser: {query1}")
    
    final_state = travel_app.invoke(
        {"messages": [HumanMessage(content=query1)]},
        config
    )
    print("\n=== Tailored Travel Advice ===")
    print(final_state["budget_estimate"])
    
    # Turn 2: Follow-up conversational query utilizing memory checkpointer
    query2 = "What weather did you find there? Remind me."
    print(f"\nUser: {query2}")
    
    follow_up_state = travel_app.invoke(
        {"messages": [HumanMessage(content=query2)]},
        config
    )
    print("\n=== Advisor Follow-Up Response ===")
    print(follow_up_state["messages"][-1].content)
```

***

## 5. Practice Exercises

### Practice 1: Add a Packing Planner Node

Extend the stateful graph by adding a third node called `packing_agent` that runs *after* the `budget_agent` node. It should read the `weather_info` from the state and suggest 3 items the user should pack for their trip.

**Instructions:**

1. Update `TravelState` to include a `packing_list` key.
2. Define a function `packing_node(state: TravelState)` that takes the `weather_info` and generates a list of 3 items to pack.
3. Add the node to the graph and update the edges: `budget_agent` $\rightarrow$ `packing_agent` $\rightarrow$ `END`.

<Accordion title="Solution">
  ```python theme={null}
  # 1. Update state schema
  class TravelState(TypedDict):
      messages: Annotated[list[BaseMessage], add_messages]
      destination: str
      weather_info: str
      budget_estimate: str
      packing_list: str # New Key

  # 2. Define Packing Node
  def packing_node(state: TravelState):
      print("\n[Node: Packing Agent] Generating packing list...")
      weather = state.get("weather_info", "")
      prompt = [
          HumanMessage(content=f"Suggest exactly 3 essential items to pack for a trip with this weather: {weather}")
      ]
      response = llm.invoke(prompt)
      return {
          "messages": [response],
          "packing_list": response.content
      }

  # 3. Add to Graph Builder
  builder = StateGraph(TravelState)
  builder.add_node("weather_agent", weather_node)
  builder.add_node("budget_agent", budget_node)
  builder.add_node("packing_agent", packing_node) # New Node

  builder.add_edge(START, "weather_agent")
  builder.add_edge("weather_agent", "budget_agent")
  builder.add_edge("budget_agent", "packing_agent") # Updated Edge
  builder.add_edge("packing_agent", END) # New Edge

  # Compile
  travel_app = builder.compile(checkpointer=MemorySaver())
  ```
</Accordion>
