Skip to content

Commit 2c07161

Browse files
committed
chore: add demo agents for showcasing subgraphs support
1 parent 3e20cf7 commit 2c07161

File tree

6 files changed

+753
-2
lines changed

6 files changed

+753
-2
lines changed

typescript-sdk/integrations/langgraph/examples/python/agents/dojo.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .agentic_chat.agent import graph as agentic_chat_graph
1616
from .agentic_generative_ui.agent import graph as agentic_generative_ui_graph
1717
from .agentic_chat_reasoning.agent import graph as agentic_chat_reasoning_graph
18+
from .subgraphs.agent import graph as subgraphs_graph
1819

1920
app = FastAPI(title="LangGraph Dojo Example Server")
2021

@@ -55,6 +56,11 @@
5556
description="An example for a reasoning chat.",
5657
graph=agentic_chat_reasoning_graph,
5758
),
59+
"subgraphs": LangGraphAgent(
60+
name="subgraphs",
61+
description="A demo of LangGraph subgraphs using a Game Character Creator.",
62+
graph=subgraphs_graph,
63+
),
5864
}
5965

6066
add_langgraph_fastapi_endpoint(
@@ -99,6 +105,12 @@
99105
path="/agent/agentic_chat_reasoning"
100106
)
101107

108+
add_langgraph_fastapi_endpoint(
109+
app=app,
110+
agent=agents["subgraphs"],
111+
path="/agent/subgraphs"
112+
)
113+
102114
def main():
103115
"""Run the uvicorn server."""
104116
port = int(os.getenv("PORT", "8000"))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Subgraphs demo module
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
"""
2+
A travel agent supervisor demo showcasing multi-agent architecture with subgraphs.
3+
The supervisor coordinates specialized agents: flights finder, hotels finder, and experiences finder.
4+
"""
5+
6+
from typing import Dict, List, Any, Optional, Annotated, Union
7+
from dataclasses import dataclass
8+
import json
9+
import os
10+
from pydantic import BaseModel, Field
11+
12+
# LangGraph imports
13+
from langchain_core.runnables import RunnableConfig
14+
from langgraph.graph import StateGraph, END, START
15+
from langgraph.types import Command, interrupt
16+
from langgraph.graph import MessagesState
17+
18+
# OpenAI imports
19+
from langchain_openai import ChatOpenAI
20+
from langchain_core.messages import SystemMessage, AIMessage
21+
22+
def create_interrupt(message: str, options: List[Any], recommendation: Any, agent: str):
23+
return interrupt({
24+
"message": message,
25+
"options": options,
26+
"recommendation": recommendation,
27+
"agent": agent,
28+
})
29+
30+
# State schema for travel planning
31+
@dataclass
32+
class Flight:
33+
airline: str
34+
departure: str
35+
arrival: str
36+
price: str
37+
duration: str
38+
39+
@dataclass
40+
class Hotel:
41+
name: str
42+
location: str
43+
price_per_night: str
44+
rating: str
45+
46+
@dataclass
47+
class Experience:
48+
name: str
49+
type: str # "restaurant" or "activity"
50+
description: str
51+
location: str
52+
53+
def merge_itinerary(left: Union[dict, None] = None, right: Union[dict, None] = None) -> dict:
54+
"""Custom reducer to merge shopping cart updates."""
55+
if not left:
56+
left = {}
57+
if not right:
58+
right = {}
59+
60+
return {**left, **right}
61+
62+
class TravelAgentState(MessagesState):
63+
"""Shared state for the travel agent system"""
64+
# Travel request details
65+
origin: str = ""
66+
destination: str = ""
67+
68+
# Results from each agent
69+
flights: List[Flight] = None
70+
hotels: List[Hotel] = None
71+
experiences: List[Experience] = None
72+
73+
itinerary: Annotated[dict, merge_itinerary] = None
74+
75+
# Tools available to all agents
76+
tools: List[Any] = None
77+
78+
# Supervisor routing
79+
next_agent: Optional[str] = None
80+
81+
# Static data for demonstration
82+
STATIC_FLIGHTS = [
83+
Flight("KLM", "Amsterdam (AMS)", "San Francisco (SFO)", "$650", "11h 30m"),
84+
Flight("United", "Amsterdam (AMS)", "San Francisco (SFO)", "$720", "12h 15m")
85+
]
86+
87+
STATIC_HOTELS = [
88+
Hotel("Hotel Zephyr", "Fisherman's Wharf", "$280/night", "4.2 stars"),
89+
Hotel("The Ritz-Carlton", "Nob Hill", "$550/night", "4.8 stars"),
90+
Hotel("Hotel Zoe", "Union Square", "$320/night", "4.4 stars")
91+
]
92+
93+
STATIC_EXPERIENCES = [
94+
Experience("Pier 39", "activity", "Iconic waterfront destination with shops and sea lions", "Fisherman's Wharf"),
95+
Experience("Golden Gate Bridge", "activity", "World-famous suspension bridge with stunning views", "Golden Gate"),
96+
Experience("Swan Oyster Depot", "restaurant", "Historic seafood counter serving fresh oysters", "Polk Street"),
97+
Experience("Tartine Bakery", "restaurant", "Artisanal bakery famous for bread and pastries", "Mission District")
98+
]
99+
100+
# Flights finder subgraph
101+
async def flights_finder(state: TravelAgentState, config: RunnableConfig):
102+
"""Subgraph that finds flight options"""
103+
104+
# Simulate flight search with static data
105+
flights = STATIC_FLIGHTS
106+
107+
selected_flight = state.get('itinerary', {}).get('flight', None)
108+
if not selected_flight:
109+
selected_flight = create_interrupt(
110+
message=f"""
111+
Found {len(flights)} flight options from {state.get('origin', 'Amsterdam')} to {state.get('destination', 'San Francisco')}.
112+
I recommend choosing the flight by {flights[0].airline} since it's known to be on time and cheaper.
113+
""",
114+
options=flights,
115+
recommendation=flights[0],
116+
agent="flights"
117+
)
118+
119+
if isinstance(selected_flight, str):
120+
selected_flight = json.loads(selected_flight)
121+
return Command(
122+
goto=END,
123+
update={
124+
"flights": flights,
125+
"itinerary": {
126+
"flight": selected_flight
127+
},
128+
"messages": state["messages"] + [{
129+
"role": "assistant",
130+
"content": f"Flights Agent: Great. I'll book you the {selected_flight["airline"]} flight from {selected_flight["departure"]} to {selected_flight["arrival"]}."
131+
}]
132+
}
133+
)
134+
135+
# Hotels finder subgraph
136+
async def hotels_finder(state: TravelAgentState, config: RunnableConfig):
137+
"""Subgraph that finds hotel options"""
138+
139+
# Simulate hotel search with static data
140+
hotels = STATIC_HOTELS
141+
selected_hotel = state.get('itinerary', {}).get('hotel', None)
142+
if not selected_hotel:
143+
selected_hotel = create_interrupt(
144+
message=f"""
145+
Found {len(hotels)} accommodation options in {state.get('destination', 'San Francisco')}.
146+
I recommend choosing the {hotels[2].name} since it strikes the balance between rating, price, and location.
147+
""",
148+
options=hotels,
149+
recommendation=hotels[2],
150+
agent="hotels"
151+
)
152+
153+
if isinstance(selected_hotel, str):
154+
selected_hotel = json.loads(selected_hotel)
155+
return Command(
156+
goto=END,
157+
update={
158+
"hotels": hotels,
159+
"itinerary": {
160+
"hotel": selected_hotel
161+
},
162+
"messages": state["messages"] + [{
163+
"role": "assistant",
164+
"content": f"Hotels Agent: Excellent choice! You'll like {selected_hotel["name"]}."
165+
}]
166+
}
167+
)
168+
169+
# Experiences finder subgraph
170+
async def experiences_finder(state: TravelAgentState, config: RunnableConfig):
171+
"""Subgraph that finds restaurant and activity recommendations"""
172+
173+
# Filter experiences (2 restaurants, 2 activities)
174+
restaurants = [exp for exp in STATIC_EXPERIENCES if exp.type == "restaurant"][:2]
175+
activities = [exp for exp in STATIC_EXPERIENCES if exp.type == "activity"][:2]
176+
experiences = restaurants + activities
177+
178+
model = ChatOpenAI(model="gpt-4o")
179+
180+
if config is None:
181+
config = RunnableConfig(recursion_limit=25)
182+
183+
itinerary = state.get("itinerary", {})
184+
185+
system_prompt = f"""
186+
You are the experiences agent. Your job is to find restaurants and activities for the user.
187+
You already went ahead and found a bunch of experiences. All you have to do now, is to let the user know of your findings.
188+
189+
Current status:
190+
- Origin: {state.get('origin', 'Amsterdam')}
191+
- Destination: {state.get('destination', 'San Francisco')}
192+
- Flight chosen: {itinerary.get("hotel", None)}
193+
- Hotel chosen: {itinerary.get("hotel", None)}
194+
- activities found: {activities}
195+
- restaurants found: {restaurants}
196+
"""
197+
198+
# Get supervisor decision
199+
response = await model.ainvoke([
200+
SystemMessage(content=system_prompt),
201+
*state["messages"],
202+
], config)
203+
204+
return Command(
205+
goto=END,
206+
update={
207+
"experiences": experiences,
208+
"messages": state["messages"] + [response]
209+
}
210+
)
211+
212+
class SupervisorResponseFormatter(BaseModel):
213+
"""Always use this tool to structure your response to the user."""
214+
answer: str = Field(description="The answer to the user")
215+
next_agent: str | None = Field(description="The agent to go to. Not required if you do not want to route to another agent.")
216+
217+
# Supervisor agent
218+
async def supervisor_agent(state: TravelAgentState, config: RunnableConfig):
219+
"""Main supervisor that coordinates all subgraphs"""
220+
221+
itinerary = state.get("itinerary", {})
222+
223+
# Check what's already completed
224+
has_flights = itinerary.get("flight", None) is not None
225+
has_hotels = itinerary.get("hotel", None) is not None
226+
has_experiences = state.get("experiences", None) is not None
227+
228+
system_prompt = f"""
229+
You are a travel planning supervisor. Your job is to coordinate specialized agents to help plan a trip.
230+
231+
Current status:
232+
- Origin: {state.get('origin', 'Amsterdam')}
233+
- Destination: {state.get('destination', 'San Francisco')}
234+
- Flights found: {has_flights}
235+
- Hotels found: {has_hotels}
236+
- Experiences found: {has_experiences}
237+
- Itinerary (Things that the user has already confirmed selection on): {json.dumps(itinerary, indent=2)}
238+
239+
Available agents:
240+
- flights_agent: Finds flight options
241+
- hotels_agent: Finds hotel options
242+
- experiences_agent: Finds restaurant and activity recommendations
243+
- {END}: Mark task as complete when all information is gathered
244+
245+
You must route to the appropriate agent based on what's missing. Once all agents have completed their tasks, route to 'complete'.
246+
"""
247+
248+
# Define the model
249+
model = ChatOpenAI(model="gpt-4o")
250+
251+
if config is None:
252+
config = RunnableConfig(recursion_limit=25)
253+
254+
# Bind the routing tool
255+
model_with_tools = model.bind_tools(
256+
[SupervisorResponseFormatter],
257+
parallel_tool_calls=False,
258+
)
259+
260+
# Get supervisor decision
261+
response = await model_with_tools.ainvoke([
262+
SystemMessage(content=system_prompt),
263+
*state["messages"],
264+
], config)
265+
266+
messages = state["messages"] + [response]
267+
268+
# Handle tool calls for routing
269+
if hasattr(response, "tool_calls") and response.tool_calls:
270+
tool_call = response.tool_calls[0]
271+
272+
if isinstance(tool_call, dict):
273+
tool_call_args = tool_call["args"]
274+
else:
275+
tool_call_args = tool_call.args
276+
277+
next_agent = tool_call_args["next_agent"]
278+
279+
# Add tool response
280+
tool_response = {
281+
"role": "tool",
282+
"content": f"Routing to {next_agent} and providing the answer",
283+
"tool_call_id": tool_call.id if hasattr(tool_call, 'id') else tool_call["id"]
284+
}
285+
286+
messages = messages + [tool_response, AIMessage(content=tool_call_args["answer"])]
287+
288+
if next_agent is not None:
289+
return Command(goto=next_agent)
290+
291+
# Fallback if no tool call
292+
return Command(
293+
goto=END,
294+
update={"messages": messages}
295+
)
296+
297+
# Create subgraphs
298+
flights_graph = StateGraph(TravelAgentState)
299+
flights_graph.add_node("flights_agent_chat_node", flights_finder)
300+
flights_graph.set_entry_point("flights_agent_chat_node")
301+
flights_graph.add_edge(START, "flights_agent_chat_node")
302+
flights_graph.add_edge("flights_agent_chat_node", END)
303+
flights_subgraph = flights_graph.compile()
304+
305+
hotels_graph = StateGraph(TravelAgentState)
306+
hotels_graph.add_node("hotels_agent_chat_node", hotels_finder)
307+
hotels_graph.set_entry_point("hotels_agent_chat_node")
308+
hotels_graph.add_edge(START, "hotels_agent_chat_node")
309+
hotels_graph.add_edge("hotels_agent_chat_node", END)
310+
hotels_subgraph = hotels_graph.compile()
311+
312+
experiences_graph = StateGraph(TravelAgentState)
313+
experiences_graph.add_node("experiences_agent_chat_node", experiences_finder)
314+
experiences_graph.set_entry_point("experiences_agent_chat_node")
315+
experiences_graph.add_edge(START, "experiences_agent_chat_node")
316+
experiences_graph.add_edge("experiences_agent_chat_node", END)
317+
experiences_subgraph = experiences_graph.compile()
318+
319+
# Main supervisor workflow
320+
workflow = StateGraph(TravelAgentState)
321+
322+
# Add supervisor and subgraphs as nodes
323+
workflow.add_node("supervisor", supervisor_agent)
324+
workflow.add_node("flights_agent", flights_subgraph)
325+
workflow.add_node("hotels_agent", hotels_subgraph)
326+
workflow.add_node("experiences_agent", experiences_subgraph)
327+
328+
# Set entry point
329+
workflow.set_entry_point("supervisor")
330+
workflow.add_edge(START, "supervisor")
331+
332+
# Add edges back to supervisor after each subgraph
333+
workflow.add_edge("flights_agent", "supervisor")
334+
workflow.add_edge("hotels_agent", "supervisor")
335+
workflow.add_edge("experiences_agent", "supervisor")
336+
337+
# Conditionally use a checkpointer based on the environment
338+
# Check for multiple indicators that we're running in LangGraph dev/API mode
339+
is_fast_api = os.environ.get("LANGGRAPH_FAST_API", "false").lower() == "true"
340+
341+
# Compile the graph
342+
if is_fast_api:
343+
# For CopilotKit and other contexts, use MemorySaver
344+
from langgraph.checkpoint.memory import MemorySaver
345+
memory = MemorySaver()
346+
graph = workflow.compile(checkpointer=memory)
347+
else:
348+
# When running in LangGraph API/dev, don't use a custom checkpointer
349+
graph = workflow.compile()

typescript-sdk/integrations/langgraph/examples/python/langgraph.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"predictive_state_updates": "./agents/predictive_state_updates/agent.py:graph",
1010
"shared_state": "./agents/shared_state/agent.py:graph",
1111
"tool_based_generative_ui": "./agents/tool_based_generative_ui/agent.py:graph",
12-
"agentic_chat_reasoning": "./agents/agentic_chat_reasoning/agent.py:graph"
12+
"agentic_chat_reasoning": "./agents/agentic_chat_reasoning/agent.py:graph",
13+
"subgraphs": "./agents/subgraphs/agent.py:graph"
1314
},
1415
"env": ".env"
1516
}

0 commit comments

Comments
 (0)