How to add multi-turn conversation in a multi-agent application (functional API)¶
Prerequisites
This guide assumes familiarity with the following:
In this how-to guide, we’ll build an application that allows an end-user to engage in a multi-turn conversation with one or more agents. We'll create a node that uses an interrupt
to collect user input and routes back to the active agent.
The agents will be implemented as tasks in a workflow that executes agent steps and determines the next action:
- Wait for user input to continue the conversation, or
- Route to another agent (or back to itself, such as in a loop) via a handoff.
from langgraph.func import entrypoint, task
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from langgraph.types import interrupt
# Define a tool to signal intent to hand off to a different agent
# Note: this is not using Command(goto) syntax for navigating to different agents:
# `workflow()` below handles the handoffs explicitly
@tool(return_direct=True)
def transfer_to_hotel_advisor():
"""Ask hotel advisor agent for help."""
return "Successfully transferred to hotel advisor"
# define an agent
travel_advisor_tools = [transfer_to_hotel_advisor, ...]
travel_advisor = create_react_agent(model, travel_advisor_tools)
# define a task that calls an agent
@task
def call_travel_advisor(messages):
response = travel_advisor.invoke({"messages": messages})
return response["messages"]
# define the multi-agent network workflow
@entrypoint(checkpointer)
def workflow(messages):
call_active_agent = call_travel_advisor
while True:
agent_messages = call_active_agent(messages).result()
ai_msg = get_last_ai_msg(agent_messages)
if not ai_msg.tool_calls:
user_input = interrupt(value="Ready for user input.")
messages = messages + [{"role": "user", "content": user_input}]
continue
messages = messages + agent_messages
call_active_agent = get_next_agent(messages)
return entrypoint.final(value=agent_messages[-1], save=messages)
API Reference: tool | entrypoint | task | create_react_agent | interrupt
Setup¶
First, let's install the required packages
import getpass
import os
def _set_env(var: str):
if not os.environ.get(var):
os.environ[var] = getpass.getpass(f"{var}: ")
_set_env("ANTHROPIC_API_KEY")
Set up LangSmith for LangGraph development
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here.
In this example we will build a team of travel assistant agents that can communicate with each other.
We will create 2 agents:
travel_advisor
: can help with travel destination recommendations. Can askhotel_advisor
for help.hotel_advisor
: can help with hotel recommendations. Can asktravel_advisor
for help.
This is a fully-connected network - every agent can talk to any other agent.
import random
from typing_extensions import Literal
from langchain_core.tools import tool
@tool
def get_travel_recommendations():
"""Get recommendation for travel destinations"""
return random.choice(["aruba", "turks and caicos"])
@tool
def get_hotel_recommendations(location: Literal["aruba", "turks and caicos"]):
"""Get hotel recommendations for a given destination."""
return {
"aruba": [
"The Ritz-Carlton, Aruba (Palm Beach)"
"Bucuti & Tara Beach Resort (Eagle Beach)"
],
"turks and caicos": ["Grace Bay Club", "COMO Parrot Cay"],
}[location]
@tool(return_direct=True)
def transfer_to_hotel_advisor():
"""Ask hotel advisor agent for help."""
return "Successfully transferred to hotel advisor"
@tool(return_direct=True)
def transfer_to_travel_advisor():
"""Ask travel advisor agent for help."""
return "Successfully transferred to travel advisor"
API Reference: tool
Transfer tools
You might have noticed that we're using @tool(return_direct=True)
in the transfer tools. This is done so that individual agents (e.g., travel_advisor
) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off immediately to a different agent.
NOTE: This is meant to work with the prebuilt create_react_agent
-- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with return_direct
.
Let's now create our agents using the the prebuilt create_react_agent
and our multi-agent workflow. Note that will be calling interrupt
every time after we get the final response from each of the agents.
import uuid
from langchain_core.messages import AIMessage
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
from langgraph.graph import add_messages
from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
# Define travel advisor ReAct agent
travel_advisor_tools = [
get_travel_recommendations,
transfer_to_hotel_advisor,
]
travel_advisor = create_react_agent(
model,
travel_advisor_tools,
state_modifier=(
"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). "
"If you need hotel recommendations, ask 'hotel_advisor' for help. "
"You MUST include human-readable response before transferring to another agent."
),
)
@task
def call_travel_advisor(messages):
# You can also add additional logic like changing the input to the agent / output from the agent, etc.
# NOTE: we're invoking the ReAct agent with the full history of messages in the state
response = travel_advisor.invoke({"messages": messages})
return response["messages"]
# Define hotel advisor ReAct agent
hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]
hotel_advisor = create_react_agent(
model,
hotel_advisor_tools,
state_modifier=(
"You are a hotel expert that can provide hotel recommendations for a given destination. "
"If you need help picking travel destinations, ask 'travel_advisor' for help."
"You MUST include human-readable response before transferring to another agent."
),
)
@task
def call_hotel_advisor(messages):
response = hotel_advisor.invoke({"messages": messages})
return response["messages"]
checkpointer = MemorySaver()
def string_to_uuid(input_string):
return str(uuid.uuid5(uuid.NAMESPACE_URL, input_string))
@entrypoint(checkpointer=checkpointer)
def multi_turn_graph(messages, previous):
previous = previous or []
messages = add_messages(previous, messages)
call_active_agent = call_travel_advisor
while True:
agent_messages = call_active_agent(messages).result()
messages = add_messages(messages, agent_messages)
# Find the last AI message
# If one of the handoff tools is called, the last message returned
# by the agent will be a ToolMessage because we set them to have
# "return_direct=True". This means that the last AIMessage will
# have tool calls.
# Otherwise, the last returned message will be an AIMessage with
# no tool calls, which means we are ready for new input.
ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))
if not ai_msg.tool_calls:
user_input = interrupt(value="Ready for user input.")
# Add user input as a human message
# NOTE: we generate unique ID for the human message based on its content
# it's important, since on subsequent invocations previous user input (interrupt) values
# will be looked up again and we will attempt to add them again here
# `add_messages` deduplicates messages based on the ID, ensuring correct message history
human_message = {
"role": "user",
"content": user_input,
"id": string_to_uuid(user_input),
}
messages = add_messages(messages, [human_message])
continue
tool_call = ai_msg.tool_calls[-1]
if tool_call["name"] == "transfer_to_hotel_advisor":
call_active_agent = call_hotel_advisor
elif tool_call["name"] == "transfer_to_travel_advisor":
call_active_agent = call_travel_advisor
else:
raise ValueError(f"Expected transfer tool, got '{tool_call['name']}'")
return entrypoint.final(value=agent_messages[-1], save=messages)
API Reference: AIMessage | ChatAnthropic | create_react_agent | add_messages | entrypoint | task | MemorySaver | interrupt | Command
Test multi-turn conversation¶
Let's test a multi turn conversation with this application.
thread_config = {"configurable": {"thread_id": uuid.uuid4()}}
inputs = [
# 1st round of conversation,
{
"role": "user",
"content": "i wanna go somewhere warm in the caribbean",
"id": str(uuid.uuid4()),
},
# Since we're using `interrupt`, we'll need to resume using the Command primitive.
# 2nd round of conversation,
Command(
resume="could you recommend a nice hotel in one of the areas and tell me which area it is."
),
# 3rd round of conversation,
Command(
resume="i like the first one. could you recommend something to do near the hotel?"
),
]
for idx, user_input in enumerate(inputs):
print()
print(f"--- Conversation Turn {idx + 1} ---")
print()
print(f"User: {user_input}")
print()
for update in multi_turn_graph.stream(
user_input,
config=thread_config,
stream_mode="updates",
):
for node_id, value in update.items():
if isinstance(value, list) and value:
last_message = value[-1]
if isinstance(last_message, dict) or last_message.type != "ai":
continue
print(f"{node_id}: {last_message.content}")
--- Conversation Turn 1 ---
User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean', 'id': 'f48d82a7-7efa-43f5-ad4c-541758c95f61'}
call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Known as "One Happy Island," Aruba offers:
- Year-round warm weather with consistent temperatures around 82°F (28°C)
- Beautiful white sand beaches like Eagle Beach and Palm Beach
- Crystal clear waters perfect for swimming and snorkeling
- Minimal rainfall and location outside the hurricane belt
- Rich culture blending Dutch and Caribbean influences
- Various activities from water sports to desert-like landscape exploration
- Excellent dining and shopping options
Would you like me to help you find suitable accommodations in Aruba? I can transfer you to our hotel advisor who can recommend specific hotels based on your preferences.
--- Conversation Turn 2 ---
User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')
call_hotel_advisor: I can recommend two excellent options in different areas:
1. The Ritz-Carlton, Aruba - Located in Palm Beach
- Luxury beachfront resort
- Located in the vibrant Palm Beach area, known for its lively atmosphere
- Close to restaurants, shopping, and nightlife
- Perfect for those who want a more active vacation with plenty of amenities nearby
2. Bucuti & Tara Beach Resort - Located in Eagle Beach
- Adults-only boutique resort
- Situated on the quieter Eagle Beach
- Known for its romantic atmosphere and excellent service
- Ideal for couples seeking a more peaceful, intimate setting
Would you like more specific information about either of these properties or their locations?
--- Conversation Turn 3 ---
User: Command(resume='i like the first one. could you recommend something to do near the hotel?')
call_travel_advisor: Near The Ritz-Carlton in Palm Beach, here are some popular activities you can enjoy:
1. Palm Beach Strip - Take a walk along this bustling strip filled with restaurants, shops, and bars
2. Visit the Bubali Bird Sanctuary - Just a short distance away
3. Try your luck at the Stellaris Casino - Located right in The Ritz-Carlton
4. Water Sports at Palm Beach - Right in front of the hotel you can:
- Go parasailing
- Try jet skiing
- Take a sunset sailing cruise
5. Visit the Palm Beach Plaza Mall - High-end shopping just a short walk away
6. Enjoy dinner at Madame Janette's - One of Aruba's most famous restaurants nearby
Would you like more specific information about any of these activities or other suggestions in the area?