In [1]:
from dotenv import load_dotenv
load_dotenv()
Out[1]:
True
In [2]:
import os, asyncio, json
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent, AgentState
from langchain.messages import HumanMessage, AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_community.utilities import SQLDatabase
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from mcp.shared.exceptions import McpError
from mcp.types import CallToolResult, TextContent
from typing import Dict, Any
from tavily import TavilyClient
from pprint import pprint
from dataclasses import dataclass
from datetime import date
DEEPSEEK_MODEL = "deepseek-chat"
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
def deepseek_model(model: str = DEEPSEEK_MODEL, max_tokens=1000, **kwargs):
return init_chat_model(
model=model,
# DeepSeek uses LangChain's OpenAI-compatible transport.
model_provider="openai",
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=DEEPSEEK_BASE_URL,
max_tokens=max_tokens,
**kwargs,
)
In [3]:
RETRYABLE_MCP_CODES = {-32603}
class RetryMCPInterceptor:
"""Intercept MCP tool calls: retry transient failures, surface all errors gracefully.
- Retryable McpError codes (e.g. -32603): retry with exponential backoff.
- Non-retryable McpError codes (e.g. -32602): return error message immediately.
- Any other exception (fetch failed, network errors, etc.): retry then return error message.
"""
def __init__(self, max_retries: int = 3):
self.max_retries = max_retries
async def __call__(self, request, handler):
last_error = None
for attempt in range(self.max_retries):
try:
return await handler(request)
except McpError as exc:
last_error = exc
print(f"[MCP interceptor] {type(exc).__name__} on {request.name} "
f"(code {exc.error.code}, attempt {attempt+1}/{self.max_retries}): {exc}")
if exc.error.code not in RETRYABLE_MCP_CODES:
return CallToolResult(
content=[TextContent(type="text", text=f"Tool call failed (non-retryable): {exc}")],
isError=False,
)
except Exception as exc:
last_error = exc
print(f"[MCP interceptor] {type(exc).__name__} on {request.name} "
f"(attempt {attempt+1}/{self.max_retries}): {exc}")
if attempt < self.max_retries - 1:
await asyncio.sleep(2 ** attempt)
print(f"[MCP interceptor] all {self.max_retries} retries exhausted for {request.name}")
return CallToolResult(
content=[TextContent(type="text", text=f"Tool call failed after {self.max_retries} attempts: {last_error}")],
isError=False,
)
In [4]:
client = MultiServerMCPClient(
{
"travel_server": {
"transport": "streamable_http",
"url": "https://mcp.kiwi.com"
}
},
tool_interceptors=[RetryMCPInterceptor()],
)
tools = await client.get_tools()
In [5]:
for msg in tools:
pprint(msg.name)
print()
'search-flight' 'feedback-to-devs'
In [6]:
tavily_client = TavilyClient()
search_count = 0
@tool
def web_search(query: str) -> Dict [str, Any]:
"""Search the web for information. You must track your search count by providing
search_number (starting at 1) and max_search_number on every call.
Queries must use only plain text characters. Do not use accented or special characters
(e.g., use 'capacite' instead of 'capacité').
"""
global search_count
search_count += 1
if search_count > 5:
return {"message": "Search limit reached. Please summarize your findings and provide your final answer."}
try:
return tavily_client.search(query)
except Exception as e:
return {"error": str(e)}
In [7]:
db = SQLDatabase.from_uri("sqlite:///resources/Chinook.db")
@tool
def query_playlist_db(query: str) -> str:
"""Query the database for playlist information"""
try:
return db.run(query)
except Exception as e:
return f"Error querying database: {e}"
Create State¶
In [8]:
class WeddingState(AgentState):
origin: str
destination: str
guest_count: str
genre: str
Create Subagents¶
In [9]:
today = date.today().isoformat()
# Travel agent
travel_agent = create_agent(
model=deepseek_model(),
tools=tools,
system_prompt=f"""
You are a travel agent. Search for flights to the desired destination wedding location.
Today is {today}. Any departureDate you pass to tools must be strictly after {today}.
You are not allowed to ask any more follow up questions, you must find the best flight options based on the following criteria:
- Price (lowest, economy class)
- Duration (shortest)
- Date (time of year which you believe is best for a wedding at this location)
To make things easy, only look for one ticket, one way.
You may need to make multiple searches to iteratively find the best options.
You will be given no extra information, only the origin and destination. It is your job to think critically about the best options.
If the MCP tool fails, returns malformed output, or does not give you usable flight results, try the tool again.
Once you have found the best options, let the user know your shortlist of options.
"""
)
In [10]:
# Venue agent
venue_agent = create_agent(
model=deepseek_model(),
tools=[web_search],
system_prompt="""
You are a venue specialist. Search for venues in the desired location, and with the desired capacity.
You are not allowed to ask any more follow up questions, you must find the best venue options based on the following criteria:
- Price (lowest)
- Capacity (exact match)
- Reviews (highest)
You may need to make multiple searches to iteratively find the best options.
You have a suggested limit of 3 web searches. Count every web_search call you make.
After 3 searches, you should stop searching and summarize the best options you have
found so far.
"""
)
In [11]:
# Playlist agent
playlist_agent = create_agent(
model=deepseek_model(),
tools=[query_playlist_db],
system_prompt="""
You are a playlist specialist. Query the sql database and curate the perfect playlist for a wedding given a genre.
Once you have your playlist, calculate the total duration and cost of the playlist, each song has an associated price.
If you run into errors when querying the database, try to fix them by making changes to the query.
Do not come back empty handed, keep trying to query the db until you find a list of songs.
This is a SQLite database. Before writing any data queries, first discover the schema.
"""
)
Main Coordinator¶
In [12]:
@tool
async def search_flights(runtime: ToolRuntime) -> str:
"""Travel agent searches for flights to the desired destination wedding location."""
origin = runtime.state["origin"]
destination = runtime.state["destination"]
response = await travel_agent.ainvoke({"messages": [HumanMessage(content=f"Find flights from {origin} to {destination}")]})
return response['messages'][-1].content
@tool
def search_venues(runtime: ToolRuntime) -> str:
"""Venue agent chooses the best venue for the given location and capacity."""
destination = runtime.state["destination"]
capacity = runtime.state["guest_count"]
query = f"Find wedding venues in {destination} for {capacity} guests"
response = venue_agent.invoke({"messages": [HumanMessage(content=query)]})
return response['messages'][-1].content
@tool
def suggest_playlist(runtime: ToolRuntime) -> str:
"""Playlist agent curates the perfect playlist for the given genre."""
genre = runtime.state["genre"]
query = f"Find {genre} tracks for wedding playlist"
response = playlist_agent.invoke({"messages": [HumanMessage(content=query)]})
return response['messages'][-1].content
@tool
def update_state(origin: str, destination: str, guest_count: str, genre: str, runtime: ToolRuntime) -> Command:
"""Update the state when you know all of the values: origin, destination, guest_count, genre.
This tool must be called alone, without any other tool calls. It must complete and return to make
the information available to other tools."""
return Command(update={
"origin": origin,
"destination": destination,
"guest_count": guest_count,
"genre": genre,
"messages": [ToolMessage("Successfully updated state", tool_call_id=runtime.tool_call_id)]}
)
In [13]:
coordinator = create_agent(
model=deepseek_model(),
tools=[search_flights, search_venues, suggest_playlist, update_state],
state_schema=WeddingState,
system_prompt="""
You are a wedding coordinator.
First find all the information you need to update the state. When you have the information, update the state.
Once that has completed and returned, you can delegate the tasks
to your specialists for flights, venues, and playlists.
Once you have received their answers, coordinate the perfect wedding for me.
"""
)
Test¶
In [14]:
response = await coordinator.ainvoke(
{
"messages": [HumanMessage(content="I'm from London and I'd like a wedding in Paris for 100 guests, jazz-genre")],
},
config={"recursion_limit": 40},
)
In [16]:
print(response["messages"][-1].content)
I've received all the information from my specialists! Here's the perfect coordinated wedding plan for you from London to Paris! 🎉 --- # 💍 Your Paris Wedding – Complete Coordination 💍 ## ✈️ Flights — London to Paris My recommendation: **LHR → ORY (London Heathrow → Paris Orly) on Wednesday 3rd June, 15:00→17:35** - **Only €40 per person!** Absolute bargain! - Arrives at **Orly Airport** — closer to central Paris than Charles de Gaulle - 1hr 35min direct flight with British Airways - Perfect mid-week arrival ahead of your wedding weekend ## 🏰 Venue — Paris I recommend **Château de Villette** (just outside Paris) for the **best value**: - ✅ **Capacity**: 150-200 guests (plenty of room for 100) - ✅ **Price**: Mid-range (~€10,000-€15,000 venue rental) - ✅ **Reviews**: ⭐⭐⭐⭐½ - ✅ Authentic **château experience** with gardens - ✅ Can sleep up to 27 guests on-site! *Alternatively*, if you prefer to be **right in central Paris**, go with the **Shangri-La Paris** (perfect 100-person capacity but luxury pricing at ~€1,000/guest). ## 🎷 Playlist — Jazz Genre Your curated **18-track jazz playlist** is ready to go! Highlights include: - **Bossa Nova classics** (The Girl from Ipanema, Corcovado) — perfect for cocktail hour - **Miles Davis** (So What, My Funny Valentine, 'Round Midnight) — sophisticated dinner music - **Upbeat numbers** (Morning Dance, Samba) — keeps the dance floor alive - **Total runtime**: ~1hr 31min | **Total cost**: Only **$17.82**! --- **Your wedding vision:** *A London couple flying to Paris on a budget-friendly flight, saying "I do" at a charming château, celebrating with smooth jazz…* It's absolutely magical! 🥂✨ *Félicitations et bon mariage!* 💕
In [21]:
search_count = 0
async for token, metadata in coordinator.astream(
{
"messages": [HumanMessage(content="I'm from London and I'd like a wedding in Paris for 100 guests, jazz-genre")],
},
config={"recursion_limit": 40},
stream_mode="messages",
):
if token.content:
print(token.content, end="", flush=True)
Great! Let me start by gathering the information I need to update the state. I have all the information: - **Origin:** London - **Destination:** Paris - **Guest Count:** 100 - **Genre:** Jazz Let me update the state first.Successfully updated stateThe state has been updated successfully! Now let me delegate to all three specialists simultaneously - flights, venues, and playlist.Now let me calculate the total duration and cost. Let me compute: **Tracks selected:** 1. **Aaron Goldberg** - "OAM's Blues" - 4:27 (266,936ms) - $0.99 2. **Aisha Duo** - "Amanda" - 4:07 (246,503ms) - $0.99 3. **Aisha Duo** - "Despertar" - 5:07 (307,385ms) - $0.99 4. **Antônio Carlos Jobim** - "Corcovado (Quiet Nights Of Quiet Stars)" - 3:26 (205,662ms) - $0.99 5. **Antônio Carlos Jobim** - "Desafinado" - 3:05 (185,338ms) - $0.99 6. **Antônio Carlos Jobim** - "Dindi (Dindi)" - 4:13 (253,178ms) - $0.99 7. **Antônio Carlos Jobim** - "Garota De Ipanema" (The Girl from Ipanema) - 4:45 (285,048ms) - $0.99 8. **Miles Davis** - "'Round Midnight" - 5:57 (357,459ms) - $0.99 9. **Miles Davis** - "Miles Ahead" - 3:30 (209,893ms) - $0.99 10. **Miles Davis** - "My Ship" - 4:28 (268,016ms) - $0.99 11. **Miles Davis** - "So What" - 9:24 (564,009ms) - $0.99 12. **Miles Davis** - "Springsville" - 3:28 (207,725ms) - $0.99 13. **Miles Davis** - "Summertime" - 3:20 (200,437ms) - $0.99 14. **Spyro Gyra** - "Believe" - 5:11 (310,778ms) - $0.99 15. **Spyro Gyra** - "Heart of the Night" - 4:34 (273,737ms) - $0.99 16. **Spyro Gyra** - "Morning Dance" - 3:58 (238,759ms) - $0.99 17. **Spyro Gyra** - "Song For Lorraine" - 4:00 (240,091ms) - $0.99 Let me calculate the totals: **Total Milliseconds:** 266936 + 246503 + 307385 + 205662 + 185338 + 253178 + 285048 + 357459 + 209893 + 268016 + 564009 + 207725 + 200437 + 310778 + 273737 + 238759 + 240091 Let me compute: 266936 + 246503 = 513439 513439 + 307385 = 820824 820824 + 205662 = 1,026,486 1,026,486 + 185338 = 1,211,824 1,211,824 + 253178 = 1,465,002 1,465,002 + 285048 = 1,750,050 1,750,050 + 357459 = 2,107,509 2,107,509 + 209893 = 2,317,402 2,317,402 + 268016 = 2,585,418 2,585,418 + 564009 = 3,149,427 3,149,427 + 207725 = 3,357,152 3,357,152 + 200437 = 3,557,589 3,557,589 + 310778 = 3,868,367 3,868,367 + 273737 = 4,142,104 4,142,104 + 238759 = 4,380,863 4,380,863 + 240091 = 4,620,954 ms **Total Duration:** 4,620,954 ms = 4,621 seconds ≈ 77 minutes (1 hour 17 minutes) **Total Cost:** 17 tracks × $0.99 = **$16.83** Here's your curated **Jazz Wedding Playlist**! 🎵💍 --- ## 💒 Jazz Wedding Playlist ### 🎷 Classic Bossa Nova (Romantic & Dreamy) — Antônio Carlos Jobim | # | Track | Duration | Price | |---|---|---|---| | 1 | **Garota De Ipanema** (The Girl from Ipanema) | 4:45 | $0.99 | | 2 | **Corcovado (Quiet Nights Of Quiet Stars)** | 3:26 | $0.99 | | 3 | **Desafinado** | 3:05 | $0.99 | | 4 | **Dindi (Dindi)** | 4:13 | $0.99 | ### 🎺 Cool Jazz (Sophisticated & Smooth) — Miles Davis | # | Track | Duration | Price | |---|---|---|---| | 5 | **Summertime** | 3:20 | $0.99 | | 6 | **My Ship** | 4:28 | $0.99 | | 7 | **Miles Ahead** | 3:30 | $0.99 | | 8 | **Springsville** | 3:28 | $0.99 | | 9 | **'Round Midnight** | 5:57 | $0.99 | | 10 | **So What** | 9:24 | $0.99 | ### 🎹 Smooth Jazz (Elegant Ambiance) — Spyro Gyra | # | Track | Duration | Price | |---|---|---|---| | 11 | **Morning Dance** | 3:58 | $0.99 | | 12 | **Heart of the Night** | 4:34 | $0.99 | | 13 | **Believe** | 5:11 | $0.99 | | 14 | **Song For Lorraine** | 4:00 | $0.99 | ### 🎼 Contemporary Jazz (Intimate & Warm) | # | Track | Duration | Price | |---|---|---|---| | 15 | **OAM's Blues** — Aaron Goldberg | 4:27 | $0.99 | | 16 | **Amanda** — Aisha Duo | 4:07 | $0.99 | | 17 | **Despertar** — Aisha Duo | 5:07 | $0.99 | --- ### 📊 Playlist Summary | Metric | Value | |---|---| | **Total Songs** | **17 tracks** | | **Total Duration** | **~1 hour 17 minutes** 🕐 | | **Total Cost** | **$16.83** 💵 | | **Genres** | Bossa Nova, Cool Jazz, Smooth Jazz, Contemporary Jazz | This playlist flows beautifully for a wedding — starting with warm, romantic bossa nova (perfect for cocktail hour or the ceremony), moving into cool jazz classics (great for dinner), and ending with smooth and contemporary jazz for a relaxed, elegant atmosphere. Enjoy the celebration! 🎉🥂I've reached my search limit. Let me summarize the best options I've found for wedding venues in Paris for 100 guests. --- ## Best Wedding Venue Options in Paris for 100 Guests Based on my research, here are the top venues to consider: ### 🏆 Top Recommendations **1. Le Pavillon Dauphine Saint Clair** - **Capacity:** 10 to 1,500 guests (multiple lounges) - perfect for 100 - **Location:** Foot of Avenue Foch, near Champs-Élysées - **Features:** Historic 1878 Chinese Pavilion, green setting, 5 refined lounges - **Price:** Mid-range (contact for quote) - **Reviews:** Well-reviewed on Bridebook - **Best for:** Elegant weddings with flexibility in space **2. Maison de l'Amérique Latine** - **Location:** Saint-Germain, central Paris - **Features:** Stunning Versailles-style gardens, Neoclassical facade, both indoor/outdoor options - **Capacity:** Suitable for ~100 guests (multiple spaces) - **Best for:** Garden ceremonies and romantic outdoor dinner receptions **3. Renaissance Paris Arc de Triomphe Hotel** - **Rating:** ⭐ 4.1/5 (71 reviews on Yelp) - **Capacity:** Suitable for 100 guests - **Feature:** Centrally located near Arc de Triomphe **4. Château de Villette** (near Paris - ~1 hour) - **Capacity:** Up to 500 guests (ideal 60-70 but works for 100) - **Price:** From ~€25,000 venue rental (~$200-$1,000/head) - **Features:** 75-hectare park, chapel, orangerie, lakes, stunning gardens - **Best for:** Those wanting a château experience near Paris ### 💰 Price Range Guide - **Budget-friendly:** €5,000-€10,000 venue rental - **Mid-range:** €10,000-€25,000 venue rental - **Luxury (Ritz, Shangri-La, Crillon):** €25,000+ (often much higher) ### ⚠️ Note on Pricing Exact prices are typically quoted upon request and depend on season, day of week, and guest count. I'd recommend: 1. Contact **Le Pavillon Dauphine** first as a great mid-range option perfectly sized for 100 guests 2. Compare with **Maison de l'Amérique Latine** for a garden setting 3. For luxury, check **Hotel Ritz Paris**, **Shangri-La Paris**, or **Hôtel de Crillon** Would you like me to help find contact details for any of these venues?Now I have a great picture of the options. Let me compile the best shortlist for you! --- ## 🎉 Paris Destination Wedding - Flight Options from London 🎉 Paris in **June** is absolutely magical for a wedding - long sunny days, blooming gardens, and that romantic summer ambiance! Here's my curated shortlist: --- ### 💰 Cheapest Options (Economy Class) | Route | Date & Time | Duration | Price | |-------|------------|----------|:-----:| | **London LHR → Paris ORY** | **Mon 01 Jun** 15:00 → 17:35 | 1h 35m | **€40** | | **London SEN → Paris CDG** | **Mon 01 Jun** 11:10 → 13:20 | 1h 10m | **€45** | | **London LGW → Paris CDG** | **Mon 01 Jun** 19:35 → 21:55 | 1h 20m | **€50** | | **London LTN → Paris CDG** | **Mon 01 Jun** 19:40 → 22:00 | 1h 20m | **€50** | | **London LGW → Paris CDG** | **Mon 15 Jun** 18:15 → 20:35 | 1h 20m | **€50** | ### ⚡ Shortest Duration Options | Route | Date & Time | Duration | Price | |-------|------------|:--------:|:-----:| | **London SEN → Paris CDG** | **Mon 01 Jun** 11:10 → 13:20 | **1h 10m** | **€45** | | **London STN → Paris CDG** | **Mon 01 Jun** 15:35 → 17:45 | **1h 10m** | **€54** | | **London STN → Paris CDG** | **Mon 08 Jun** 15:35 → 17:45 | **1h 10m** | **€54** | | **London STN → Paris CDG** | **Mon 15 Jun** 15:35 → 17:45 | **1h 10m** | **€54** | --- ### 🌟 My Top Recommendations **🥇 Best Overall Pick:** **Mon 1st June - London LHR → Paris ORY at €40** — From Heathrow to Orly for just €40! Absolute steal, and Orly is closer to central Paris than CDG. **🥇 Shortest & Cheapest Combo:** **Mon 1st June - London SEN → Paris CDG at €45 — only 1h 10min flight!** **🥇 Best for a Wedding Trip:** **Early June** is perfect — the weather in Paris is gorgeous (20-25°C), gardens are in full bloom, and it's prime wedding season in the City of Love! 🌸 --- **Fun fact about Paris:** Did you know there's a "Wall of Love" (Le Mur des Je t'aime) in Montmartre with **"I love you" written in 312 languages**? The perfect romantic spot for a wedding photo! 💕 Wishing you a magical Parisian wedding adventure! 🥂✨I've received all the information from my specialists! Let me coordinate the perfect Paris wedding for you. Here's everything: --- # 🎉 Your Perfect London-to-Paris Wedding Plan 🎉 **Dear Client,** Congratulations on your upcoming wedding! Here's your fully coordinated Parisian wedding package, curated just for you — **100 guests, jazz vibes, and a short hop from London to Paris!** --- ## ✈️ Flights — London → Paris | Option | Route | Time | Duration | Price | |--------|-------|------|:--------:|:-----:| | 🥇 **Best Pick** | London **LHR** → Paris **ORY** | Mon 1 Jun, 15:00→17:35 | 1h 35m | **€40/person** | | 🥇 **Shortest** | London **SEN** → Paris **CDG** | Mon 1 Jun, 11:10→13:20 | **1h 10m** | **€45/person** | > **Recommendation:** The €40 Heathrow→Orly flight is a steal! Plus Orly is closer to central Paris than CDG ✨ --- ## 🏛️ Venue — Paris for 100 Guests ### Top Choice: **Le Pavillon Dauphine Saint Clair** - **Capacity:** 10–1,500 guests *(perfect for your 100!)* - **Location:** Avenue Foch, near Champs-Élysées - **Style:** Historic 1878 Chinese Pavilion with 5 elegant lounges in a green setting - **Price:** Mid-range (by request) ### Also consider: - **Maison de l'Amérique Latine** — Stunning Versailles-style gardens in Saint-Germain - **Château de Villette** — A fairytale château experience ~1hr from Paris (from €25,000) --- ## 🎵 Jazz Wedding Playlist Here's your curated **17-track jazz playlist** (~1h 17min of music, **$16.83**): | Mood | Artist | Tracks | |------|--------|--------| | 🎸 **Romantic Bossa Nova** | Antônio Carlos Jobim | *Garota de Ipanema, Corcovado, Desafinado, Dindi* | | 🎺 **Cool Jazz** | Miles Davis | *Summertime, 'Round Midnight, So What, My Ship...* | | 🎹 **Smooth Jazz** | Spyro Gyra | *Morning Dance, Heart of the Night, Believe...* | | 🎼 **Contemporary Jazz** | Aaron Goldberg, Aisha Duo | *OAM's Blues, Amanda, Despertar* | --- ## 💍 The Perfect Wedding Timeline | Time | Activity | |:----:|----------| | **11:10** 🛫 | **Guests fly from London (SEN) to Paris (CDG)** — just 1h 10min! | | **13:20** 🛬 | **Arrive in Paris** — transfer to the venue | | **15:00** 🌸 | **Ceremony** at Le Pavillon Dauphine | | **16:00** 🥂 | **Cocktail hour** with Bossa Nova jazz | | **18:00** 🍽️ | **Reception & Dinner** with Miles Davis cool jazz | | **20:00** 💃 | **Dancing** with smooth & contemporary jazz | | **Late** 🎆 | **Magical Parisian evening** under the stars! | --- **Bon voyage et félicitations!** 🥂💕 Your Paris wedding is going to be absolutely magical!
In [ ]: