Merge branch 'main' into v1
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to extract images from the graph-api.ipynb notebook and save them to assets folder.
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def extract_images_from_notebook(notebook_path, assets_dir):
|
||||
"""Extract images from notebook and save them to assets directory."""
|
||||
|
||||
# Read the notebook
|
||||
with open(notebook_path, 'r') as f:
|
||||
notebook = json.load(f)
|
||||
|
||||
# Create assets directory if it doesn't exist
|
||||
os.makedirs(assets_dir, exist_ok=True)
|
||||
|
||||
image_count = 0
|
||||
|
||||
# Process each cell
|
||||
for cell_idx, cell in enumerate(notebook['cells']):
|
||||
if cell['cell_type'] == 'code':
|
||||
# Check if this cell contains draw_mermaid_png
|
||||
source = ''.join(cell.get('source', []))
|
||||
if 'draw_mermaid_png' in source:
|
||||
print(f"Found draw_mermaid_png in cell {cell_idx}")
|
||||
|
||||
# Check for outputs with images
|
||||
if 'outputs' in cell:
|
||||
for output_idx, output in enumerate(cell['outputs']):
|
||||
if output.get('output_type') == 'display_data':
|
||||
data = output.get('data', {})
|
||||
|
||||
# Check for PNG data
|
||||
if 'image/png' in data:
|
||||
png_data = data['image/png']
|
||||
|
||||
# Decode base64 data
|
||||
try:
|
||||
image_bytes = base64.b64decode(png_data)
|
||||
|
||||
# Generate filename
|
||||
image_count += 1
|
||||
filename = f"graph_api_image_{image_count}.png"
|
||||
filepath = os.path.join(assets_dir, filename)
|
||||
|
||||
# Save the image
|
||||
with open(filepath, 'wb') as img_file:
|
||||
img_file.write(image_bytes)
|
||||
|
||||
print(f"Saved image: {filepath}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error decoding image {image_count}: {e}")
|
||||
|
||||
print(f"Extracted {image_count} images to {assets_dir}")
|
||||
return image_count
|
||||
|
||||
if __name__ == "__main__":
|
||||
notebook_path = "docs/docs/how-tos/graph-api.ipynb"
|
||||
assets_dir = "docs/docs/how-tos/assets"
|
||||
|
||||
if os.path.exists(notebook_path):
|
||||
count = extract_images_from_notebook(notebook_path, assets_dir)
|
||||
print(f"Successfully extracted {count} images")
|
||||
else:
|
||||
print(f"Notebook not found: {notebook_path}")
|
||||
@@ -0,0 +1,11 @@
|
||||
# Additional resources
|
||||
|
||||
This section contains additional resources for LangGraph.
|
||||
|
||||
- [Community agents](../agents/prebuilt.md): A collection of prebuilt libraries that you can use in your LangGraph applications.
|
||||
- [LangGraph Academy](https://academy.langchain.com/courses/intro-to-langgraph): A collection of courses that teach you how to use LangGraph.
|
||||
- [Case studies](../adopters.md): A collection of case studies that show how LangGraph is used in production.
|
||||
- [FAQ](../concepts/faq.md): A collection of frequently asked questions about LangGraph.
|
||||
- [llms.txt](../llms-txt-overview.md): A list of documentation files in the `llms.txt` format that allow LLMs and agents to access our documentation.
|
||||
- [LangChain Forum](https://forum.langchain.com/): A place to ask questions and get help from other LangGraph users.
|
||||
- [Troubleshooting](../troubleshooting/errors/index.md.md): A collection of troubleshooting guides for common issues.
|
||||
@@ -52,7 +52,7 @@ agent.invoke(
|
||||
)
|
||||
```
|
||||
|
||||
1. Define a tool for the agent to use. Tools can be defined as vanilla Python functions. For more advanced tool usage and customization, check the [tools](./tools.md) page.
|
||||
1. Define a tool for the agent to use. Tools can be defined as vanilla Python functions. For more advanced tool usage and customization, check the [tools](../how-tos/tool-calling.md) page.
|
||||
2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) page.
|
||||
3. Provide a list of tools for the model to use.
|
||||
4. Provide a system prompt (instructions) to the language model used by the agent.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Threads
|
||||
|
||||
A thread contains the accumulated state of a sequence of [runs](../../concepts/assistants.md#execution). When a run is executed, the [state](../../concepts/low_level.md#state) of the underlying graph of the assistant will be persisted to the thread.
|
||||
|
||||
A thread's current and historical state can be retrieved. To persist state, a thread must be created prior to executing a run.
|
||||
|
||||
The state of a thread at a particular point in time is called a [checkpoint](../../concepts/persistence.md#checkpoints). Checkpoints are persisted and can be used to restore the state of a thread at a later time.
|
||||
|
||||
## Learn more
|
||||
|
||||
* For more on threads and checkpoints, see this section of the [LangGraph conceptual guide](../../concepts/persistence.md).
|
||||
* The LangGraph Platform API provides several endpoints for creating and managing threads and thread state. See the [API reference](../../cloud/reference/api/api_ref.html#tag/threads) for more details.
|
||||
@@ -26,7 +26,7 @@ The primary benefits of using multi-agent systems are:
|
||||
There are several ways to connect agents in a multi-agent system:
|
||||
|
||||
- **Network**: each agent can communicate with [every other agent](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/). Any agent can decide which other agent to call next.
|
||||
- **Supervisor**: each agent communicates with a single [supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) agent. Supervisor agent makes decisions on which agent should be called next.
|
||||
- **Supervisor**: each agent communicates with a single [supervisor](../tutorials/multi_agent/agent_supervisor.md) agent. Supervisor agent makes decisions on which agent should be called next.
|
||||
- **Supervisor (tool-calling)**: this is a special case of supervisor architecture. Individual agents can be represented as tools. In this case, a supervisor agent uses a tool-calling LLM to decide which of the agent tools to call, as well as the arguments to pass to those agents.
|
||||
- **Hierarchical**: you can define a multi-agent system with [a supervisor of supervisors](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/). This is a generalization of the supervisor architecture and allows for more complex control flows.
|
||||
- **Custom multi-agent workflow**: each agent communicates with only a subset of agents. Parts of the flow are deterministic, and only some agents can decide which other agents to call next.
|
||||
@@ -211,7 +211,7 @@ builder.add_edge(START, "supervisor")
|
||||
supervisor = builder.compile()
|
||||
```
|
||||
|
||||
Check out this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) for an example of supervisor multi-agent architecture.
|
||||
Check out this [tutorial](../tutorials/multi_agent/agent_supervisor.md) for an example of supervisor multi-agent architecture.
|
||||
|
||||
### Supervisor (tool-calling)
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ To make user-scoped tools available to your LangGraph Platform deployment, start
|
||||
```python
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
|
||||
def get_mcp_tools_node(state, config):
|
||||
def mcp_tools_node(state, config):
|
||||
user = config["configurable"].get("langgraph_auth_user")
|
||||
# e.g., user["github_token"], user["email"], etc.
|
||||
|
||||
@@ -223,14 +223,19 @@ def get_mcp_tools_node(state, config):
|
||||
}
|
||||
})
|
||||
tools = await client.get_tools() # (3)
|
||||
return {"tools": tools}
|
||||
|
||||
|
||||
# Your tool-calling logic here
|
||||
|
||||
tool_messages = ...
|
||||
return {"messages": tool_messages}
|
||||
```
|
||||
|
||||
1. MCP only supports adding headers to requests made to `streamable_http` and `sse` `transport` servers.
|
||||
2. Your MCP server URL.
|
||||
3. Get available tools from your MCP server.
|
||||
|
||||
_This can also be done by [rebuilding your graph at runtime](https://langchain-ai.github.io/langgraph/cloud/deployment/graph_rebuild/) to have a different configuration for a new run_
|
||||
|
||||
## Session behavior
|
||||
|
||||
The current LangGraph MCP implementation does not support sessions. Each `/mcp` request is stateless and independent.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Examples
|
||||
|
||||
The pages in this section provide end-to-end examples for the following topics:
|
||||
|
||||
## General
|
||||
|
||||
- [Template Applications](../concepts/template_applications.md): Create a LangGraph application from a template.
|
||||
- [Agentic RAG](./rag/langgraph_agentic_rag.md): Build a retrieval agent that can decide when to use a retriever tool.
|
||||
- [Agent Supervisor](./multi_agent/agent_supervisor.md): Build a supervisor agent that can manage a team of agents.
|
||||
- [SQL agent](./sql/sql-agent.md): Build a SQL agent that can execute SQL queries and return the results.
|
||||
- [Prebuilt chat UI](../agents/ui.md): Use a prebuilt chat UI to interact with any LangGraph agent.
|
||||
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.md): Use LangSmith to track and analyze graph runs.
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
- [Set up custom authentication](./auth/getting_started.md): Set up custom authentication for your LangGraph application.
|
||||
- [Make conversations private](./auth/resource_auth.md): Make conversations private by using resource-based authentication.
|
||||
- [Connect an authentication provider](./auth/add_auth_server.md): Connect an authentication provider to your LangGraph application.
|
||||
- [Rebuild graph at runtime](../cloud/deployment/graph_rebuild.md): Rebuild a graph at runtime.
|
||||
- [Use RemoteGraph](../how-tos/use-remote-graph.md): Use RemoteGraph to deploy your LangGraph application to a remote server.
|
||||
- [Deploy CrewAI, AutoGen, and other frameworks](../how-tos/autogen-integration.md): Deploy CrewAI, AutoGen, and other frameworks with LangGraph.
|
||||
- [Integrate LangGraph into a React app](../cloud/how-tos/use_stream_react.md)
|
||||
- [Implement Generative User Interfaces with LangGraph](../cloud/how-tos/generative_ui_react.md)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Guides
|
||||
|
||||
The pages in this section provide a conceptual overview and how-tos for the following topics:
|
||||
|
||||
## LangGraph APIs
|
||||
|
||||
- [Graph API](../concepts/low_level.md): Use the Graph API to define workflows using a graph paradigm.
|
||||
- [Functional API](../concepts/functional_api.md): Use Functional API to build workflows using a functional paradigm without thinking about the graph structure.
|
||||
- [Runtime](../concepts/pregel.md): Pregel implements LangGraph's runtime, managing the execution of LangGraph applications.
|
||||
|
||||
## Core capabilities
|
||||
|
||||
These capabilities are available in both LangGraph OSS and the LangGraph Platform.
|
||||
|
||||
- [Streaming](../concepts/streaming.md): Stream outputs from a LangGraph graph.
|
||||
- [Persistence](../concepts/persistence.md): Persist the state of a LangGraph graph.
|
||||
- [Durable execution](../concepts/durable_execution.md): Save progress at key points in the graph execution.
|
||||
- [Memory](../concepts/memory.md): Remember information about previous interactions.
|
||||
- [Context](../agents/context.md): Pass outside data to a LangGraph graph to provide context for the graph execution.
|
||||
- [Models](../agents/models.md): Integrate various LLMs into your LangGraph application.
|
||||
- [Tools](../concepts/tools.md): Interface directly with external systems.
|
||||
- [Human-in-the-loop](../concepts/human_in_the_loop.md): Enable human intervention at any point in a workflow.
|
||||
- [Breakpoints](../concepts/breakpoints.md): Pause the execution of a LangGraph graph at a specific point.
|
||||
- [Time travel](../concepts/time-travel.md): Travel back in time to a specific point in the execution of a LangGraph graph.
|
||||
- [Subgraphs](../concepts/subgraphs.md): Build modular graphs.
|
||||
- [Multi-agent](../concepts/multi_agent.md): Break down a complex workflow into multiple agents.
|
||||
- [MCP](../concepts/mcp.md): Use MCP servers in a LangGraph graph.
|
||||
- [Evaluation](../agents/evals.md): Use LangSmith to evaluate your graph's performance.
|
||||
|
||||
## Platform-only capabilities
|
||||
|
||||
These capabilities are only available in [LangGraph Platform](../concepts/langgraph_platform.md).
|
||||
|
||||
- [Authentication and access control](../concepts/auth.md): Authenticate and authorize users to access a Langraph graph.
|
||||
- [Assistants](../concepts/assistants.md): Build assistants that can be used to interact with a LangGraph graph.
|
||||
- [Double-texting](../concepts/double_texting.md): Handle double-texting (consecutive messages before a first response is returned) in a LangGraph graph.
|
||||
- [Webhooks](../cloud/concepts/webhooks.md): Send webhooks to a LangGraph graph.
|
||||
- [Cron jobs](../cloud/concepts/cron_jobs.md): Schedule jobs to run at a specific time.
|
||||
- [Server customization](../how-tos/http/custom_lifespan.md): Customize the server that runs a LangGraph graph.
|
||||
- [Data management](../cloud/concepts/data_storage_and_privacy.md): Manage data in a LangGraph graph.
|
||||
- [Deployment](../concepts/deployment_options.md): Deploy a LangGraph graph to a server.
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,321 @@
|
||||
# How to integrate LangGraph with AutoGen, CrewAI, and other frameworks
|
||||
|
||||
This guide shows how to integrate AutoGen agents with LangGraph to leverage features like persistence, streaming, and memory, and then deploy the integrated solution to LangGraph Platform for scalable production use. In this guide we show how to build a LangGraph chatbot that integrates with AutoGen, but you can follow the same approach with other frameworks.
|
||||
|
||||
Integrating AutoGen with LangGraph provides several benefits:
|
||||
|
||||
- Enhanced features: Add [persistence](../concepts/persistence.md), [streaming](../concepts/streaming.md), [short and long-term memory](../concepts/memory.md) and more to your AutoGen agents.
|
||||
- Multi-agent systems: Build [multi-agent systems](../concepts/multi_agent.md) where individual agents are built with different frameworks.
|
||||
- Production deployment: Deploy your integrated solution to [LangGraph Platform](../concepts/langgraph_platform.md) for scalable production use.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.9+
|
||||
- Autogen: `pip install autogen`
|
||||
- LangGraph: `pip install langgraph`
|
||||
- OpenAI API key
|
||||
|
||||
## Setup
|
||||
|
||||
Set your your environment:
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
|
||||
def _set_env(var: str):
|
||||
if not os.environ.get(var):
|
||||
os.environ[var] = getpass.getpass(f"{var}: ")
|
||||
|
||||
|
||||
_set_env("OPENAI_API_KEY")
|
||||
```
|
||||
|
||||
## 1. Define AutoGen agent
|
||||
|
||||
Create an AutoGen agent that can execute code. This example is adapted from AutoGen's [official tutorials](https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb):
|
||||
|
||||
```python
|
||||
import autogen
|
||||
import os
|
||||
|
||||
config_list = [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}]
|
||||
|
||||
llm_config = {
|
||||
"timeout": 600,
|
||||
"cache_seed": 42,
|
||||
"config_list": config_list,
|
||||
"temperature": 0,
|
||||
}
|
||||
|
||||
autogen_agent = autogen.AssistantAgent(
|
||||
name="assistant",
|
||||
llm_config=llm_config,
|
||||
)
|
||||
|
||||
user_proxy = autogen.UserProxyAgent(
|
||||
name="user_proxy",
|
||||
human_input_mode="NEVER",
|
||||
max_consecutive_auto_reply=10,
|
||||
is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"),
|
||||
code_execution_config={
|
||||
"work_dir": "web",
|
||||
"use_docker": False,
|
||||
}, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.
|
||||
llm_config=llm_config,
|
||||
system_message="Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.",
|
||||
)
|
||||
```
|
||||
|
||||
## 2. Create the graph
|
||||
|
||||
We will now create a LangGraph chatbot graph that calls AutoGen agent.
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_openai_messages
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
def call_autogen_agent(state: MessagesState):
|
||||
# Convert LangGraph messages to OpenAI format for AutoGen
|
||||
messages = convert_to_openai_messages(state["messages"])
|
||||
|
||||
# Get the last user message
|
||||
last_message = messages[-1]
|
||||
|
||||
# Pass previous message history as context (excluding the last message)
|
||||
carryover = messages[:-1] if len(messages) > 1 else []
|
||||
|
||||
# Initiate chat with AutoGen
|
||||
response = user_proxy.initiate_chat(
|
||||
autogen_agent,
|
||||
message=last_message,
|
||||
carryover=carryover
|
||||
)
|
||||
|
||||
# Extract the final response from the agent
|
||||
final_content = response.chat_history[-1]["content"]
|
||||
|
||||
# Return the response in LangGraph format
|
||||
return {"messages": {"role": "assistant", "content": final_content}}
|
||||
|
||||
# Create the graph with memory for persistence
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Build the graph
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node("autogen", call_autogen_agent)
|
||||
builder.add_edge(START, "autogen")
|
||||
|
||||
# Compile with checkpointer for persistence
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
```
|
||||
|
||||
```python
|
||||
from IPython.display import display, Image
|
||||
|
||||
display(Image(graph.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 3. Test the graph locally
|
||||
|
||||
Before deploying to LangGraph Platform, you can test the graph locally:
|
||||
|
||||
```python
|
||||
# pass the thread ID to persist agent outputs for future interactions
|
||||
# highlight-next-line
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
for chunk in graph.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Find numbers between 10 and 30 in fibonacci sequence",
|
||||
}
|
||||
]
|
||||
},
|
||||
# highlight-next-line
|
||||
config,
|
||||
):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
user_proxy (to assistant):
|
||||
|
||||
Find numbers between 10 and 30 in fibonacci sequence
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
assistant (to user_proxy):
|
||||
|
||||
To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:
|
||||
|
||||
1. Generate Fibonacci numbers starting from 0.
|
||||
2. Continue generating until the numbers exceed 30.
|
||||
3. Collect and print the numbers that are between 10 and 30.
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Since we're leveraging LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) features we can now continue the conversation using the same thread ID -- LangGraph will automatically pass previous history to the AutoGen agent:
|
||||
|
||||
```python
|
||||
for chunk in graph.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Multiply the last number by 3",
|
||||
}
|
||||
]
|
||||
},
|
||||
# highlight-next-line
|
||||
config,
|
||||
):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
user_proxy (to assistant):
|
||||
|
||||
Multiply the last number by 3
|
||||
Context:
|
||||
Find numbers between 10 and 30 in fibonacci sequence
|
||||
The Fibonacci numbers between 10 and 30 are 13 and 21.
|
||||
|
||||
These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1.
|
||||
|
||||
The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
|
||||
|
||||
As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.
|
||||
|
||||
TERMINATE
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
assistant (to user_proxy):
|
||||
|
||||
The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:
|
||||
|
||||
21 * 3 = 63
|
||||
|
||||
TERMINATE
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
{'call_autogen_agent': {'messages': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n\n21 * 3 = 63\n\nTERMINATE'}}}
|
||||
```
|
||||
|
||||
## 4. Prepare for deployment
|
||||
|
||||
To deploy to LangGraph Platform, create a file structure like the following:
|
||||
|
||||
```
|
||||
my-autogen-agent/
|
||||
├── agent.py # Your main agent code
|
||||
├── requirements.txt # Python dependencies
|
||||
└── langgraph.json # LangGraph configuration
|
||||
```
|
||||
|
||||
=== "agent.py"
|
||||
|
||||
```python
|
||||
import os
|
||||
import autogen
|
||||
from langchain_core.messages import convert_to_openai_messages
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
# AutoGen configuration
|
||||
config_list = [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}]
|
||||
|
||||
llm_config = {
|
||||
"timeout": 600,
|
||||
"cache_seed": 42,
|
||||
"config_list": config_list,
|
||||
"temperature": 0,
|
||||
}
|
||||
|
||||
# Create AutoGen agents
|
||||
autogen_agent = autogen.AssistantAgent(
|
||||
name="assistant",
|
||||
llm_config=llm_config,
|
||||
)
|
||||
|
||||
user_proxy = autogen.UserProxyAgent(
|
||||
name="user_proxy",
|
||||
human_input_mode="NEVER",
|
||||
max_consecutive_auto_reply=10,
|
||||
is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"),
|
||||
code_execution_config={
|
||||
"work_dir": "/tmp/autogen_work",
|
||||
"use_docker": False,
|
||||
},
|
||||
llm_config=llm_config,
|
||||
system_message="Reply TERMINATE if the task has been solved at full satisfaction.",
|
||||
)
|
||||
|
||||
def call_autogen_agent(state: MessagesState):
|
||||
"""Node function that calls the AutoGen agent"""
|
||||
messages = convert_to_openai_messages(state["messages"])
|
||||
last_message = messages[-1]
|
||||
carryover = messages[:-1] if len(messages) > 1 else []
|
||||
|
||||
response = user_proxy.initiate_chat(
|
||||
autogen_agent,
|
||||
message=last_message,
|
||||
carryover=carryover
|
||||
)
|
||||
|
||||
final_content = response.chat_history[-1]["content"]
|
||||
return {"messages": {"role": "assistant", "content": final_content}}
|
||||
|
||||
# Create and compile the graph
|
||||
def create_graph():
|
||||
checkpointer = MemorySaver()
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node("autogen", call_autogen_agent)
|
||||
builder.add_edge(START, "autogen")
|
||||
return builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Export the graph for LangGraph Platform
|
||||
graph = create_graph()
|
||||
```
|
||||
|
||||
=== "requirements.txt"
|
||||
|
||||
```
|
||||
langgraph>=0.1.0
|
||||
pyautogen>=0.2.0
|
||||
langchain-core>=0.1.0
|
||||
langchain-openai>=0.0.5
|
||||
```
|
||||
|
||||
=== "langgraph.json"
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"autogen_agent": "./agent.py:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 5. Deploy to LangGraph Platform
|
||||
|
||||
Deploy the graph with the LangGraph Platform CLI:
|
||||
|
||||
```
|
||||
pip install -U langgraph-cli
|
||||
```
|
||||
|
||||
```
|
||||
langgraph deploy --config langgraph.json
|
||||
```
|
||||
@@ -1,171 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8381b6e0-29a6-48c5-b451-5d2549351249",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks\n",
|
||||
"\n",
|
||||
"[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) provides infrastructure for deploying agents. This integrates seamlessly with LangGraph, but can also work with other frameworks. The way to make this work is to wrap the agent in a single LangGraph node, and have that be the entire graph.\n",
|
||||
"\n",
|
||||
"Doing so will allow you to deploy to LangGraph Platform, and allows you to get a lot of the [benefits](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). You get horizontally scalable infrastructure, a task queue to handle bursty operations, a persistence layer to power short term memory, and long term memory support.\n",
|
||||
"\n",
|
||||
"In this guide we show how to do this with an AutoGen agent, but this method should work for agents defined in other frameworks like CrewAI, LlamaIndex, and others as well."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1113cb16-b538-448c-924c-85731ce96ebd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "f05993fa-9d03-4f45-bc13-0a8d87260d86",
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install autogen langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f4e0ca12-1714-4776-a30a-9527e519799b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define autogen agent\n",
|
||||
"\n",
|
||||
"Here we define our AutoGen agent. From https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d4a14dc7-d565-4207-8788-525f85b9fb27",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import autogen\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
|
||||
"\n",
|
||||
"llm_config = {\n",
|
||||
" \"timeout\": 600,\n",
|
||||
" \"cache_seed\": 42,\n",
|
||||
" \"config_list\": config_list,\n",
|
||||
" \"temperature\": 0,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"autogen_agent = autogen.AssistantAgent(\n",
|
||||
" name=\"assistant\",\n",
|
||||
" llm_config=llm_config,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"user_proxy = autogen.UserProxyAgent(\n",
|
||||
" name=\"user_proxy\",\n",
|
||||
" human_input_mode=\"NEVER\",\n",
|
||||
" max_consecutive_auto_reply=10,\n",
|
||||
" is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
|
||||
" code_execution_config={\n",
|
||||
" \"work_dir\": \"web\",\n",
|
||||
" \"use_docker\": False,\n",
|
||||
" }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
|
||||
" llm_config=llm_config,\n",
|
||||
" system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b1170836-f23e-4e4c-ab83-ce791cd7fbd2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Wrap in LangGraph\n",
|
||||
"\n",
|
||||
"We now wrap the AutoGen agent in a single LangGraph node, and make that the entire graph.\n",
|
||||
"The main thing this involves is defining an Input and Output schema for the node, which you would need to do if deploying this manually, so it's no extra work"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "7b417c16-ff4e-4d5c-a9a9-0aaeeef6ede5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, MessagesState\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_autogen_agent(state: MessagesState):\n",
|
||||
" last_message = state[\"messages\"][-1]\n",
|
||||
" response = user_proxy.initiate_chat(autogen_agent, message=last_message.content)\n",
|
||||
" # get the final response from the agent\n",
|
||||
" content = response.chat_history[-1][\"content\"]\n",
|
||||
" return {\"messages\": {\"role\": \"assistant\", \"content\": content}}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph = StateGraph(MessagesState)\n",
|
||||
"graph.add_node(call_autogen_agent)\n",
|
||||
"graph.set_entry_point(\"call_autogen_agent\")\n",
|
||||
"graph = graph.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f6a18377-ac29-478f-a76a-b213f1a3c85d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Deploy with LangGraph Platform\n",
|
||||
"\n",
|
||||
"You can now deploy this as you normally would with LangGraph Platform. See [these instructions](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for more details."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "34d3d54e-9a2b-481e-bccd-74aca7a53f9a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Build multi-agent systems"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "3f0b4f70-f14e-4026-82c0-874786789ee8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A single agent might struggle if it needs to specialize in multiple domains or manage many tools. To tackle this, you can break your agent into smaller, independent agents and composing them into a [multi-agent system](../../concepts/multi_agent).\n",
|
||||
"\n",
|
||||
"In multi-agent systems, agents need to communicate between each other. They do so via [handoffs](#handoffs) — a primitive that describes which agent to hand control to and the payload to send to that agent.\n",
|
||||
"\n",
|
||||
"This guide covers the following:\n",
|
||||
"\n",
|
||||
"* implementing [handoffs](#handoffs) between agents\n",
|
||||
"* using handoffs and the prebuilt [agent](../../agents/agents) to [build a custom multi-agent system](#build-a-multi-agent-system)\n",
|
||||
"\n",
|
||||
"To get started with building multi-agent systems, check out LangGraph [prebuilt implementations](#prebuilt-implementations) of two of the most popular multi-agent architectures — [supervisor](../../agents/multi-agent#supervisor) and [swarm](../../agents/multi-agent#swarm)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "7d43e110-16fc-4899-97f1-015d5b804b87",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Handoffs\n",
|
||||
"\n",
|
||||
"To set up communication between the agents in a multi-agent system you can use [**handoffs**](../../concepts/multi_agent#handoffs) — a pattern where one agent *hands off* control to another. Handoffs allow you to specify:\n",
|
||||
"\n",
|
||||
"- **destination**: target agent to navigate to (e.g., name of the LangGraph node to go to)\n",
|
||||
"- **payload**: information to pass to that agent (e.g., state update)\n",
|
||||
"\n",
|
||||
"### Create handoffs\n",
|
||||
"\n",
|
||||
"To implement handoffs, you can return [`Command`](../command) objects from your agent nodes or tools:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from typing import Annotated\n",
|
||||
"from langchain_core.tools import tool, InjectedToolCallId\n",
|
||||
"from langgraph.prebuilt import create_react_agent, InjectedState\n",
|
||||
"from langgraph.graph import StateGraph, START, MessagesState\n",
|
||||
"from langgraph.types import Command\n",
|
||||
"\n",
|
||||
"def create_handoff_tool(*, agent_name: str, description: str | None = None):\n",
|
||||
" name = f\"transfer_to_{agent_name}\"\n",
|
||||
" description = description or f\"Transfer to {agent_name}\"\n",
|
||||
"\n",
|
||||
" @tool(name, description=description)\n",
|
||||
" def handoff_tool(\n",
|
||||
" # highlight-next-line\n",
|
||||
" state: Annotated[MessagesState, InjectedState], # (1)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" tool_call_id: Annotated[str, InjectedToolCallId],\n",
|
||||
" ) -> Command:\n",
|
||||
" tool_message = {\n",
|
||||
" \"role\": \"tool\",\n",
|
||||
" \"content\": f\"Successfully transferred to {agent_name}\",\n",
|
||||
" \"name\": name,\n",
|
||||
" \"tool_call_id\": tool_call_id,\n",
|
||||
" }\n",
|
||||
" return Command( # (2)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" goto=agent_name, # (3)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" update={\"messages\": state[\"messages\"] + [tool_message]}, # (4)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" graph=Command.PARENT, # (5)!\n",
|
||||
" )\n",
|
||||
" return handoff_tool\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"1. Access the [state](../../concepts/low_level#state) of the agent that is calling the handoff tool using the [InjectedState][langgraph.prebuilt.InjectedState] annotation. See [this guide](../tool-calling/#read-state) for more information.\n",
|
||||
"2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.\n",
|
||||
"3. Name of the agent or node to hand off to.\n",
|
||||
"4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.\n",
|
||||
"5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.\n",
|
||||
"\n",
|
||||
"!!! tip\n",
|
||||
"\n",
|
||||
" If you want to use tools that return `Command`, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:\n",
|
||||
" \n",
|
||||
" ```python\n",
|
||||
" def call_tools(state):\n",
|
||||
" ...\n",
|
||||
" commands = [tools_by_name[tool_call[\"name\"]].invoke(tool_call) for tool_call in tool_calls]\n",
|
||||
" return commands\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"!!! Important\n",
|
||||
"\n",
|
||||
" This handoff implementation assumes that:\n",
|
||||
" \n",
|
||||
" - each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs)\n",
|
||||
" - each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" def call_hotel_assistant(state):\n",
|
||||
" # return agent's final response,\n",
|
||||
" # excluding inner monologue\n",
|
||||
" response = hotel_assistant.invoke(state)\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"messages\": response[\"messages\"][-1]}\n",
|
||||
" ```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3956f12d-285a-4799-a0a5-db13def58a15",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Control agent inputs\n",
|
||||
"\n",
|
||||
"You can use the [`Send()`][langgraph.types.Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"\n",
|
||||
"from typing import Annotated\n",
|
||||
"from langchain_core.tools import tool, InjectedToolCallId\n",
|
||||
"from langgraph.prebuilt import InjectedState\n",
|
||||
"from langgraph.graph import StateGraph, START, MessagesState\n",
|
||||
"# highlight-next-line\n",
|
||||
"from langgraph.types import Command, Send\n",
|
||||
"\n",
|
||||
"def create_task_description_handoff_tool(\n",
|
||||
" *, agent_name: str, description: str | None = None\n",
|
||||
"):\n",
|
||||
" name = f\"transfer_to_{agent_name}\"\n",
|
||||
" description = description or f\"Ask {agent_name} for help.\"\n",
|
||||
"\n",
|
||||
" @tool(name, description=description)\n",
|
||||
" def handoff_tool(\n",
|
||||
" # this is populated by the calling agent\n",
|
||||
" task_description: Annotated[\n",
|
||||
" str,\n",
|
||||
" \"Description of what the next agent should do, including all of the relevant context.\",\n",
|
||||
" ],\n",
|
||||
" # these parameters are ignored by the LLM\n",
|
||||
" state: Annotated[MessagesState, InjectedState],\n",
|
||||
" ) -> Command:\n",
|
||||
" task_description_message = {\"role\": \"user\", \"content\": task_description}\n",
|
||||
" agent_input = {**state, \"messages\": [task_description_message]}\n",
|
||||
" return Command(\n",
|
||||
" # highlight-next-line\n",
|
||||
" goto=[Send(agent_name, agent_input)],\n",
|
||||
" graph=Command.PARENT,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" return handoff_tool\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"See the multi-agent [supervisor](../tutorials/agent_supervisor.ipynb#4-create-delegation-tasks) tutorial for a full example of using [`Send()`][langgraph.types.Send] in handoffs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "21511f57-7bf3-4223-9a17-ce9fc84c40ab",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Build a multi-agent system\n",
|
||||
"\n",
|
||||
"You can use handoffs in any agents built with LangGraph. We recommend using the prebuilt [agent](../../agents/overview) or [`ToolNode`](../tool-calling#use-prebuilt-toolnode), as they natively support handoffs tools returning `Command`. Below is an example of how you can implement a multi-agent system for booking travel using handoffs:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.graph import StateGraph, START, MessagesState\n",
|
||||
"\n",
|
||||
"def create_handoff_tool(*, agent_name: str, description: str | None = None):\n",
|
||||
" # same implementation as above\n",
|
||||
" ...\n",
|
||||
" return Command(...)\n",
|
||||
"\n",
|
||||
"# Handoffs\n",
|
||||
"transfer_to_hotel_assistant = create_handoff_tool(agent_name=\"hotel_assistant\")\n",
|
||||
"transfer_to_flight_assistant = create_handoff_tool(agent_name=\"flight_assistant\")\n",
|
||||
"\n",
|
||||
"# Define agents\n",
|
||||
"flight_assistant = create_react_agent(\n",
|
||||
" model=\"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" tools=[..., transfer_to_hotel_assistant],\n",
|
||||
" # highlight-next-line\n",
|
||||
" name=\"flight_assistant\"\n",
|
||||
")\n",
|
||||
"hotel_assistant = create_react_agent(\n",
|
||||
" model=\"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" tools=[..., transfer_to_flight_assistant],\n",
|
||||
" # highlight-next-line\n",
|
||||
" name=\"hotel_assistant\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Define multi-agent graph\n",
|
||||
"multi_agent_graph = (\n",
|
||||
" StateGraph(MessagesState)\n",
|
||||
" # highlight-next-line\n",
|
||||
" .add_node(flight_assistant)\n",
|
||||
" # highlight-next-line\n",
|
||||
" .add_node(hotel_assistant)\n",
|
||||
" .add_edge(START, \"flight_assistant\")\n",
|
||||
" .compile()\n",
|
||||
")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"??? example \"Full example: Multi-agent system for booking travel\"\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from typing import Annotated\n",
|
||||
" from langchain_core.messages import convert_to_messages\n",
|
||||
" from langchain_core.tools import tool, InjectedToolCallId\n",
|
||||
" from langgraph.prebuilt import create_react_agent, InjectedState\n",
|
||||
" from langgraph.graph import StateGraph, START, MessagesState\n",
|
||||
" from langgraph.types import Command\n",
|
||||
" \n",
|
||||
" # We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely\n",
|
||||
" \n",
|
||||
" def pretty_print_message(message, indent=False):\n",
|
||||
" pretty_message = message.pretty_repr(html=True)\n",
|
||||
" if not indent:\n",
|
||||
" print(pretty_message)\n",
|
||||
" return\n",
|
||||
" \n",
|
||||
" indented = \"\\n\".join(\"\\t\" + c for c in pretty_message.split(\"\\n\"))\n",
|
||||
" print(indented)\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" def pretty_print_messages(update, last_message=False):\n",
|
||||
" is_subgraph = False\n",
|
||||
" if isinstance(update, tuple):\n",
|
||||
" ns, update = update\n",
|
||||
" # skip parent graph updates in the printouts\n",
|
||||
" if len(ns) == 0:\n",
|
||||
" return\n",
|
||||
" \n",
|
||||
" graph_id = ns[-1].split(\":\")[0]\n",
|
||||
" print(f\"Update from subgraph {graph_id}:\")\n",
|
||||
" print(\"\\n\")\n",
|
||||
" is_subgraph = True\n",
|
||||
" \n",
|
||||
" for node_name, node_update in update.items():\n",
|
||||
" update_label = f\"Update from node {node_name}:\"\n",
|
||||
" if is_subgraph:\n",
|
||||
" update_label = \"\\t\" + update_label\n",
|
||||
" \n",
|
||||
" print(update_label)\n",
|
||||
" print(\"\\n\")\n",
|
||||
" \n",
|
||||
" messages = convert_to_messages(node_update[\"messages\"])\n",
|
||||
" if last_message:\n",
|
||||
" messages = messages[-1:]\n",
|
||||
" \n",
|
||||
" for m in messages:\n",
|
||||
" pretty_print_message(m, indent=is_subgraph)\n",
|
||||
" print(\"\\n\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
" def create_handoff_tool(*, agent_name: str, description: str | None = None):\n",
|
||||
" name = f\"transfer_to_{agent_name}\"\n",
|
||||
" description = description or f\"Transfer to {agent_name}\"\n",
|
||||
" \n",
|
||||
" @tool(name, description=description)\n",
|
||||
" def handoff_tool(\n",
|
||||
" # highlight-next-line\n",
|
||||
" state: Annotated[MessagesState, InjectedState], # (1)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" tool_call_id: Annotated[str, InjectedToolCallId],\n",
|
||||
" ) -> Command:\n",
|
||||
" tool_message = {\n",
|
||||
" \"role\": \"tool\",\n",
|
||||
" \"content\": f\"Successfully transferred to {agent_name}\",\n",
|
||||
" \"name\": name,\n",
|
||||
" \"tool_call_id\": tool_call_id,\n",
|
||||
" }\n",
|
||||
" return Command( # (2)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" goto=agent_name, # (3)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" update={\"messages\": state[\"messages\"] + [tool_message]}, # (4)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" graph=Command.PARENT, # (5)!\n",
|
||||
" )\n",
|
||||
" return handoff_tool\n",
|
||||
" \n",
|
||||
" # Handoffs\n",
|
||||
" transfer_to_hotel_assistant = create_handoff_tool(\n",
|
||||
" agent_name=\"hotel_assistant\",\n",
|
||||
" description=\"Transfer user to the hotel-booking assistant.\",\n",
|
||||
" )\n",
|
||||
" transfer_to_flight_assistant = create_handoff_tool(\n",
|
||||
" agent_name=\"flight_assistant\",\n",
|
||||
" description=\"Transfer user to the flight-booking assistant.\",\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Simple agent tools\n",
|
||||
" def book_hotel(hotel_name: str):\n",
|
||||
" \"\"\"Book a hotel\"\"\"\n",
|
||||
" return f\"Successfully booked a stay at {hotel_name}.\"\n",
|
||||
" \n",
|
||||
" def book_flight(from_airport: str, to_airport: str):\n",
|
||||
" \"\"\"Book a flight\"\"\"\n",
|
||||
" return f\"Successfully booked a flight from {from_airport} to {to_airport}.\"\n",
|
||||
" \n",
|
||||
" # Define agents\n",
|
||||
" flight_assistant = create_react_agent(\n",
|
||||
" model=\"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" tools=[book_flight, transfer_to_hotel_assistant],\n",
|
||||
" prompt=\"You are a flight booking assistant\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" name=\"flight_assistant\"\n",
|
||||
" )\n",
|
||||
" hotel_assistant = create_react_agent(\n",
|
||||
" model=\"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" tools=[book_hotel, transfer_to_flight_assistant],\n",
|
||||
" prompt=\"You are a hotel booking assistant\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" name=\"hotel_assistant\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Define multi-agent graph\n",
|
||||
" multi_agent_graph = (\n",
|
||||
" StateGraph(MessagesState)\n",
|
||||
" .add_node(flight_assistant)\n",
|
||||
" .add_node(hotel_assistant)\n",
|
||||
" .add_edge(START, \"flight_assistant\")\n",
|
||||
" .compile()\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Run the multi-agent graph\n",
|
||||
" for chunk in multi_agent_graph.stream(\n",
|
||||
" {\n",
|
||||
" \"messages\": [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"book a flight from BOS to JFK and a stay at McKittrick Hotel\"\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # highlight-next-line\n",
|
||||
" subgraphs=True\n",
|
||||
" ):\n",
|
||||
" pretty_print_messages(chunk)\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
" 1. Access agent's state\n",
|
||||
" 2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.\n",
|
||||
" 3. Name of the agent or node to hand off to.\n",
|
||||
" 4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.\n",
|
||||
" 5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e8314da4-9971-429b-9e70-58b40795de74",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Multi-turn conversation\n",
|
||||
"\n",
|
||||
"Users might want to engage in a *multi-turn conversation* with one or more agents. To build a system that can handle this, you can create a node that uses an [`interrupt`][langgraph.types.interrupt] to collect user input and routes back to the **active** agent.\n",
|
||||
"\n",
|
||||
"The agents can then be implemented as nodes in a graph that executes agent steps and determines the next action:\n",
|
||||
"\n",
|
||||
"1. **Wait for user input** to continue the conversation, or \n",
|
||||
"2. **Route to another agent** (or back to itself, such as in a loop) via a [handoff](#handoffs)\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def human(state) -> Command[Literal[\"agent\", \"another_agent\"]]:\n",
|
||||
" \"\"\"A node for collecting user input.\"\"\"\n",
|
||||
" user_input = interrupt(value=\"Ready for user input.\")\n",
|
||||
"\n",
|
||||
" # Determine the active agent.\n",
|
||||
" active_agent = ...\n",
|
||||
"\n",
|
||||
" ...\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"human\",\n",
|
||||
" \"content\": user_input,\n",
|
||||
" }]\n",
|
||||
" },\n",
|
||||
" goto=active_agent\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"def agent(state) -> Command[Literal[\"agent\", \"another_agent\", \"human\"]]:\n",
|
||||
" # The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.\n",
|
||||
" goto = get_next_agent(...) # 'agent' / 'another_agent'\n",
|
||||
" if goto:\n",
|
||||
" return Command(goto=goto, update={\"my_state_key\": \"my_state_value\"})\n",
|
||||
" else:\n",
|
||||
" return Command(goto=\"human\") # Go to human node\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"??? example \"Full example: multi-agent system for travel recommendations\"\n",
|
||||
"\n",
|
||||
" In this example, we will build a team of travel assistant agents that can communicate with each other via handoffs.\n",
|
||||
" \n",
|
||||
" We will create 2 agents:\n",
|
||||
" \n",
|
||||
" * travel_advisor: can help with travel destination recommendations. Can ask hotel_advisor for help.\n",
|
||||
" * hotel_advisor: can help with hotel recommendations. Can ask travel_advisor for help.\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langchain_anthropic import ChatAnthropic\n",
|
||||
" from langgraph.graph import MessagesState, StateGraph, START\n",
|
||||
" from langgraph.prebuilt import create_react_agent, InjectedState\n",
|
||||
" from langgraph.types import Command, interrupt\n",
|
||||
" from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
" class MultiAgentState(MessagesState):\n",
|
||||
" last_active_agent: str\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" # Define travel advisor tools and ReAct agent\n",
|
||||
" travel_advisor_tools = [\n",
|
||||
" get_travel_recommendations,\n",
|
||||
" make_handoff_tool(agent_name=\"hotel_advisor\"),\n",
|
||||
" ]\n",
|
||||
" travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" def call_travel_advisor(\n",
|
||||
" state: MultiAgentState,\n",
|
||||
" ) -> Command[Literal[\"hotel_advisor\", \"human\"]]:\n",
|
||||
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
|
||||
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
|
||||
" response = travel_advisor.invoke(state)\n",
|
||||
" update = {**response, \"last_active_agent\": \"travel_advisor\"}\n",
|
||||
" return Command(update=update, goto=\"human\")\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" # Define hotel advisor tools and ReAct agent\n",
|
||||
" hotel_advisor_tools = [\n",
|
||||
" get_hotel_recommendations,\n",
|
||||
" make_handoff_tool(agent_name=\"travel_advisor\"),\n",
|
||||
" ]\n",
|
||||
" hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" def call_hotel_advisor(\n",
|
||||
" state: MultiAgentState,\n",
|
||||
" ) -> Command[Literal[\"travel_advisor\", \"human\"]]:\n",
|
||||
" response = hotel_advisor.invoke(state)\n",
|
||||
" update = {**response, \"last_active_agent\": \"hotel_advisor\"}\n",
|
||||
" return Command(update=update, goto=\"human\")\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" def human_node(\n",
|
||||
" state: MultiAgentState, config\n",
|
||||
" ) -> Command[Literal[\"hotel_advisor\", \"travel_advisor\", \"human\"]]:\n",
|
||||
" \"\"\"A node for collecting user input.\"\"\"\n",
|
||||
" \n",
|
||||
" user_input = interrupt(value=\"Ready for user input.\")\n",
|
||||
" active_agent = state[\"last_active_agent\"]\n",
|
||||
" \n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" {\n",
|
||||
" \"role\": \"human\",\n",
|
||||
" \"content\": user_input,\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" goto=active_agent,\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" builder = StateGraph(MultiAgentState)\n",
|
||||
" builder.add_node(\"travel_advisor\", call_travel_advisor)\n",
|
||||
" builder.add_node(\"hotel_advisor\", call_hotel_advisor)\n",
|
||||
" \n",
|
||||
" # This adds a node to collect human input, which will route\n",
|
||||
" # back to the active agent.\n",
|
||||
" builder.add_node(\"human\", human_node)\n",
|
||||
" \n",
|
||||
" # We'll always start with a general travel advisor.\n",
|
||||
" builder.add_edge(START, \"travel_advisor\")\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" checkpointer = MemorySaver()\n",
|
||||
" graph = builder.compile(checkpointer=checkpointer)\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
" Let's test a multi turn conversation with this application.\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" import uuid\n",
|
||||
" \n",
|
||||
" thread_config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
|
||||
" \n",
|
||||
" inputs = [\n",
|
||||
" # 1st round of conversation,\n",
|
||||
" {\n",
|
||||
" \"messages\": [\n",
|
||||
" {\"role\": \"user\", \"content\": \"i wanna go somewhere warm in the caribbean\"}\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # Since we're using `interrupt`, we'll need to resume using the Command primitive.\n",
|
||||
" # 2nd round of conversation,\n",
|
||||
" Command(\n",
|
||||
" resume=\"could you recommend a nice hotel in one of the areas and tell me which area it is.\"\n",
|
||||
" ),\n",
|
||||
" # 3rd round of conversation,\n",
|
||||
" Command(\n",
|
||||
" resume=\"i like the first one. could you recommend something to do near the hotel?\"\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
" \n",
|
||||
" for idx, user_input in enumerate(inputs):\n",
|
||||
" print()\n",
|
||||
" print(f\"--- Conversation Turn {idx + 1} ---\")\n",
|
||||
" print()\n",
|
||||
" print(f\"User: {user_input}\")\n",
|
||||
" print()\n",
|
||||
" for update in graph.stream(\n",
|
||||
" user_input,\n",
|
||||
" config=thread_config,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
" ):\n",
|
||||
" for node_id, value in update.items():\n",
|
||||
" if isinstance(value, dict) and value.get(\"messages\", []):\n",
|
||||
" last_message = value[\"messages\"][-1]\n",
|
||||
" if isinstance(last_message, dict) or last_message.type != \"ai\":\n",
|
||||
" continue\n",
|
||||
" print(f\"{node_id}: {last_message.content}\")\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
" ```\n",
|
||||
" --- Conversation Turn 1 ---\n",
|
||||
" \n",
|
||||
" User: {'messages': [{'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}]}\n",
|
||||
" \n",
|
||||
" travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known as \"One Happy Island\" and offers:\n",
|
||||
" - Year-round warm weather with consistent temperatures around 82°F (28°C)\n",
|
||||
" - Beautiful white sand beaches like Eagle Beach and Palm Beach\n",
|
||||
" - Clear turquoise waters perfect for swimming and snorkeling\n",
|
||||
" - Minimal rainfall and location outside the hurricane belt\n",
|
||||
" - A blend of Caribbean and Dutch culture\n",
|
||||
" - Great dining options and nightlife\n",
|
||||
" - Various water sports and activities\n",
|
||||
" \n",
|
||||
" Would you like me to get some specific hotel recommendations in Aruba for your stay? I can transfer you to our hotel advisor who can help with accommodations.\n",
|
||||
" \n",
|
||||
" --- Conversation Turn 2 ---\n",
|
||||
" \n",
|
||||
" User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')\n",
|
||||
" \n",
|
||||
" hotel_advisor: Based on the recommendations, I can suggest two excellent options:\n",
|
||||
" \n",
|
||||
" 1. The Ritz-Carlton, Aruba - Located in Palm Beach\n",
|
||||
" - This luxury resort is situated in the vibrant Palm Beach area\n",
|
||||
" - Known for its exceptional service and amenities\n",
|
||||
" - Perfect if you want to be close to dining, shopping, and entertainment\n",
|
||||
" - Features multiple restaurants, a casino, and a world-class spa\n",
|
||||
" - Located on a pristine stretch of Palm Beach\n",
|
||||
" \n",
|
||||
" 2. Bucuti & Tara Beach Resort - Located in Eagle Beach\n",
|
||||
" - An adults-only boutique resort on Eagle Beach\n",
|
||||
" - Known for being more intimate and peaceful\n",
|
||||
" - Award-winning for its sustainability practices\n",
|
||||
" - Perfect for a romantic getaway or peaceful vacation\n",
|
||||
" - Located on one of the most beautiful beaches in the Caribbean\n",
|
||||
" \n",
|
||||
" Would you like more specific information about either of these properties or their locations?\n",
|
||||
" \n",
|
||||
" --- Conversation Turn 3 ---\n",
|
||||
" \n",
|
||||
" User: Command(resume='i like the first one. could you recommend something to do near the hotel?')\n",
|
||||
" \n",
|
||||
" travel_advisor: Near the Ritz-Carlton in Palm Beach, here are some highly recommended activities:\n",
|
||||
" \n",
|
||||
" 1. Visit the Palm Beach Plaza Mall - Just a short walk from the hotel, featuring shopping, dining, and entertainment\n",
|
||||
" 2. Try your luck at the Stellaris Casino - It's right in the Ritz-Carlton\n",
|
||||
" 3. Take a sunset sailing cruise - Many depart from the nearby pier\n",
|
||||
" 4. Visit the California Lighthouse - A scenic landmark just north of Palm Beach\n",
|
||||
" 5. Enjoy water sports at Palm Beach:\n",
|
||||
" - Jet skiing\n",
|
||||
" - Parasailing\n",
|
||||
" - Snorkeling\n",
|
||||
" - Stand-up paddleboarding\n",
|
||||
" \n",
|
||||
" Would you like more specific information about any of these activities or would you like to know about other options in the area?\n",
|
||||
" ```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "04d18c63-a0eb-45ac-86dc-0cc5bd683973",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Prebuilt implementations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e0e4ce57-f8de-4f37-836e-c1e1a02dd7b7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"LangGraph comes with prebuilt implementations of two of the most popular multi-agent architectures:\n",
|
||||
"\n",
|
||||
"- [supervisor](../../agents/multi-agent#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent systems.\n",
|
||||
"- [swarm](../../agents/multi-agent#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -130,7 +130,7 @@ def create_task_description_handoff_tool(
|
||||
return handoff_tool
|
||||
```
|
||||
|
||||
See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.ipynb#4-create-delegation-tasks) example for a full example of using [`Send()`][langgraph.types.Send] in handoffs.
|
||||
See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.md#4-create-delegation-tasks) example for a full example of using [`Send()`][langgraph.types.Send] in handoffs.
|
||||
|
||||
## Build a multi-agent system
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# How to pass custom run ID or set tags and metadata for graph runs in LangSmith
|
||||
|
||||
!!! tip "Prerequisites"
|
||||
This guide assumes familiarity with the following:
|
||||
|
||||
- [LangSmith Documentation](https://docs.smith.langchain.com)
|
||||
- [LangSmith Platform](https://smith.langchain.com)
|
||||
- [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig)
|
||||
- [Add metadata and tags to traces](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain#add-metadata-and-tags-to-traces)
|
||||
- [Customize run name](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain#customize-run-name)
|
||||
|
||||
Debugging graph runs can sometimes be difficult to do in an IDE or terminal. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read the [LangSmith documentation](https://docs.smith.langchain.com) for more information on how to get started.
|
||||
|
||||
To make it easier to identify and analyzed traces generated during graph invocation, you can set additional configuration at run time (see [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig)):
|
||||
|
||||
| **Field** | **Type** | **Description** |
|
||||
|-------------|---------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| run_name | `str` | Name for the tracer run for this call. Defaults to the name of the class. |
|
||||
| run_id | `UUID` | Unique identifier for the tracer run for this call. If not provided, a new UUID will be generated. |
|
||||
| tags | `List[str]` | Tags for this call and any sub-calls (e.g., a Chain calling an LLM). You can use these to filter calls. |
|
||||
| metadata | `Dict[str, Any]` | Metadata for this call and any sub-calls (e.g., a Chain calling an LLM). Keys should be strings, values should be JSON-serializable. |
|
||||
|
||||
LangGraph graphs implement the [LangChain Runnable Interface](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.base.Runnable.html) and accept a second argument (`RunnableConfig`) in methods like `invoke`, `ainvoke`, `stream` etc.
|
||||
|
||||
The LangSmith platform will allow you to search and filter traces based on `run_name`, `run_id`, `tags` and `metadata`.
|
||||
|
||||
## TLDR
|
||||
|
||||
```python
|
||||
import uuid
|
||||
# Generate a random UUID -- it must be a UUID
|
||||
config = {"run_id": uuid.uuid4()}, "tags": ["my_tag1"], "metadata": {"a": 5}}
|
||||
# Works with all standard Runnable methods
|
||||
# like invoke, batch, ainvoke, astream_events etc
|
||||
graph.stream(inputs, config, stream_mode="values")
|
||||
```
|
||||
|
||||
The rest of the how to guide will show a full agent.
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's install the required packages and set our API keys
|
||||
|
||||
```python
|
||||
%%capture --no-stderr
|
||||
%pip install --quiet -U langgraph langchain_openai
|
||||
```
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
|
||||
def _set_env(var: str):
|
||||
if not os.environ.get(var):
|
||||
os.environ[var] = getpass.getpass(f"{var}: ")
|
||||
|
||||
|
||||
_set_env("OPENAI_API_KEY")
|
||||
_set_env("LANGSMITH_API_KEY")
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com).
|
||||
|
||||
## Define the graph
|
||||
|
||||
For this example we will use the [prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/).
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from typing import Literal
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# First we initialize the model we want to use.
|
||||
model = ChatOpenAI(model="gpt-4o", temperature=0)
|
||||
|
||||
|
||||
# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)
|
||||
@tool
|
||||
def get_weather(city: Literal["nyc", "sf"]):
|
||||
"""Use this to get weather information."""
|
||||
if city == "nyc":
|
||||
return "It might be cloudy in nyc"
|
||||
elif city == "sf":
|
||||
return "It's always sunny in sf"
|
||||
else:
|
||||
raise AssertionError("Unknown city")
|
||||
|
||||
|
||||
tools = [get_weather]
|
||||
|
||||
|
||||
# Define the graph
|
||||
graph = create_react_agent(model, tools=tools)
|
||||
```
|
||||
|
||||
## Run your graph
|
||||
|
||||
Now that we've defined our graph let's run it once and view the trace in LangSmith. In order for our trace to be easily accessible in LangSmith, we will pass in a custom `run_id` in the config.
|
||||
|
||||
This assumes that you have set your `LANGSMITH_API_KEY` environment variable.
|
||||
|
||||
Note that you can also configure what project to trace to by setting the `LANGCHAIN_PROJECT` environment variable, by default runs will be traced to the `default` project.
|
||||
|
||||
```python
|
||||
import uuid
|
||||
|
||||
|
||||
def print_stream(stream):
|
||||
for s in stream:
|
||||
message = s["messages"][-1]
|
||||
if isinstance(message, tuple):
|
||||
print(message)
|
||||
else:
|
||||
message.pretty_print()
|
||||
|
||||
|
||||
inputs = {"messages": [("user", "what is the weather in sf")]}
|
||||
|
||||
config = {"run_name": "agent_007", "tags": ["cats are awesome"]}
|
||||
|
||||
print_stream(graph.stream(inputs, config, stream_mode="values"))
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================ Human Message ==================================
|
||||
|
||||
what is the weather in sf
|
||||
================================== Ai Message ===================================
|
||||
Tool Calls:
|
||||
get_weather (call_9ZudXyMAdlUjptq9oMGtQo8o)
|
||||
Call ID: call_9ZudXyMAdlUjptq9oMGtQo8o
|
||||
Args:
|
||||
city: sf
|
||||
================================= Tool Message ==================================
|
||||
Name: get_weather
|
||||
|
||||
It's always sunny in sf
|
||||
================================== Ai Message ===================================
|
||||
|
||||
The weather in San Francisco is currently sunny.
|
||||
```
|
||||
|
||||
## View the trace in LangSmith
|
||||
|
||||
Now that we've ran our graph, let's head over to LangSmith and view our trace. First click into the project that you traced to (in our case the default project). You should see a run with the custom run name "agent_007".
|
||||
|
||||

|
||||
|
||||
In addition, you will be able to filter traces after the fact using the tags or metadata provided. For example,
|
||||
|
||||

|
||||
@@ -17,7 +17,7 @@ Safari blocks plain-HTTP traffic on localhost. When running Studio with `langgra
|
||||
|
||||
```shell
|
||||
# Requires @langchain/langgraph-cli>=0.0.26
|
||||
npx @langchain/langgraph-cli dev
|
||||
npx @langchain/langgraph-cli dev --tunnel
|
||||
```
|
||||
|
||||
The command outputs a URL in this format:
|
||||
@@ -55,7 +55,7 @@ Disable Brave Shields for LangSmith using the Brave icon in the URL bar.
|
||||
|
||||
```shell
|
||||
# Requires @langchain/langgraph-cli>=0.0.26
|
||||
npx @langchain/langgraph-cli dev
|
||||
npx @langchain/langgraph-cli dev --tunnel
|
||||
```
|
||||
|
||||
The command outputs a URL in this format:
|
||||
|
||||
@@ -0,0 +1,812 @@
|
||||
# Multi-agent supervisor
|
||||
|
||||
[**Supervisor**](../../concepts/multi_agent.md#supervisor) is a multi-agent architecture where **specialized** agents are coordinated by a central **supervisor agent**. The supervisor agent controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements.
|
||||
|
||||
In this tutorial, you will build a supervisor system with two agents — a research and a math expert. By the end of the tutorial you will:
|
||||
|
||||
1. Build specialized research and math agents
|
||||
2. Build a supervisor for orchestrating them with the prebuilt [`langgraph-supervisor`](https://langchain-ai.github.io/langgraph/agents/multi-agent/#supervisor)
|
||||
3. Build a supervisor from scratch
|
||||
4. Implement advanced task delegation
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
First, let's install required packages and set our API keys
|
||||
|
||||
```python
|
||||
%%capture --no-stderr
|
||||
%pip install -U langgraph langgraph-supervisor langchain-tavily "langchain[openai]"
|
||||
```
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
|
||||
def _set_if_undefined(var: str):
|
||||
if not os.environ.get(var):
|
||||
os.environ[var] = getpass.getpass(f"Please provide your {var}")
|
||||
|
||||
|
||||
_set_if_undefined("OPENAI_API_KEY")
|
||||
_set_if_undefined("TAVILY_API_KEY")
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph.
|
||||
|
||||
## 1. Create worker agents
|
||||
|
||||
First, let's create our specialized worker agents — research agent and math agent:
|
||||
|
||||
* Research agent will have access to a web search tool using [Tavily API](https://tavily.com/)
|
||||
* Math agent will have access to simple math tools (`add`, `multiply`, `divide`)
|
||||
|
||||
### Research agent
|
||||
|
||||
For web search, we will use `TavilySearch` tool from `langchain-tavily`:
|
||||
|
||||
```python
|
||||
from langchain_tavily import TavilySearch
|
||||
|
||||
web_search = TavilySearch(max_results=3)
|
||||
web_search_results = web_search.invoke("who is the mayor of NYC?")
|
||||
|
||||
print(web_search_results["results"][0]["content"])
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Find events, attractions, deals, and more at nyctourism.com Skip Main Navigation Menu The Official Website of the City of New York Text Size Powered by Translate SearchSearch Primary Navigation The official website of NYC Home NYC Resources NYC311 Office of the Mayor Events Connect Jobs Search Office of the Mayor | Mayor's Bio | City of New York Secondary Navigation MayorBiographyNewsOfficials Eric L. Adams 110th Mayor of New York City Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve public safety, and build a stronger, healthier city that delivers for all New Yorkers. As the representative of one of the nation's largest counties, Eric fought tirelessly to grow the local economy, invest in schools, reduce inequality, improve public safety, and advocate for smart policies and better government that delivers for all New Yorkers.
|
||||
```
|
||||
|
||||
To create individual worker agents, we will use LangGraph's prebuilt [agent](../../agents/agents.md).
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
research_agent = create_react_agent(
|
||||
model="openai:gpt-4.1",
|
||||
tools=[web_search],
|
||||
prompt=(
|
||||
"You are a research agent.\n\n"
|
||||
"INSTRUCTIONS:\n"
|
||||
"- Assist ONLY with research-related tasks, DO NOT do any math\n"
|
||||
"- After you're done with your tasks, respond to the supervisor directly\n"
|
||||
"- Respond ONLY with the results of your work, do NOT include ANY other text."
|
||||
),
|
||||
name="research_agent",
|
||||
)
|
||||
```
|
||||
|
||||
Let's [run the agent](../../agents/run_agents.md) to verify that it behaves as expected.
|
||||
|
||||
!!! note "We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely"
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
|
||||
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
|
||||
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
|
||||
print(update_label)
|
||||
print("\n")
|
||||
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
```
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
|
||||
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
|
||||
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
|
||||
print(update_label)
|
||||
print("\n")
|
||||
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
```
|
||||
|
||||
```python
|
||||
for chunk in research_agent.stream(
|
||||
{"messages": [{"role": "user", "content": "who is the mayor of NYC?"}]}
|
||||
):
|
||||
pretty_print_messages(chunk)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: research_agent
|
||||
Tool Calls:
|
||||
tavily_search (call_U748rQhQXT36sjhbkYLSXQtJ)
|
||||
Call ID: call_U748rQhQXT36sjhbkYLSXQtJ
|
||||
Args:
|
||||
query: current mayor of New York City
|
||||
search_depth: basic
|
||||
|
||||
|
||||
Update from node tools:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: tavily_search
|
||||
|
||||
{"query": "current mayor of New York City", "follow_up_questions": null, "answer": null, "images": [], "results": [{"title": "List of mayors of New York City - Wikipedia", "url": "https://en.wikipedia.org/wiki/List_of_mayors_of_New_York_City", "content": "The mayor of New York City is the chief executive of the Government of New York City, as stipulated by New York City's charter.The current officeholder, the 110th in the sequence of regular mayors, is Eric Adams, a member of the Democratic Party.. During the Dutch colonial period from 1624 to 1664, New Amsterdam was governed by the Director of Netherland.", "score": 0.9039154, "raw_content": null}, {"title": "Office of the Mayor | Mayor's Bio | City of New York - NYC.gov", "url": "https://www.nyc.gov/office-of-the-mayor/bio.page", "content": "Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve", "score": 0.8405867, "raw_content": null}, {"title": "Eric Adams - Wikipedia", "url": "https://en.wikipedia.org/wiki/Eric_Adams", "content": "Eric Leroy Adams (born September 1, 1960) is an American politician and former police officer who has served as the 110th mayor of New York City since 2022. Adams was an officer in the New York City Transit Police and then the New York City Police Department (```
|
||||
```
|
||||
|
||||
### Math agent
|
||||
|
||||
For math agent tools we will use [vanilla Python functions](../../how-tos/tool-calling.md#define-a-tool):
|
||||
|
||||
```python
|
||||
def add(a: float, b: float):
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
|
||||
def multiply(a: float, b: float):
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
|
||||
def divide(a: float, b: float):
|
||||
"""Divide two numbers."""
|
||||
return a / b
|
||||
|
||||
|
||||
math_agent = create_react_agent(
|
||||
model="openai:gpt-4.1",
|
||||
tools=[add, multiply, divide],
|
||||
prompt=(
|
||||
"You are a math agent.\n\n"
|
||||
"INSTRUCTIONS:\n"
|
||||
"- Assist ONLY with math-related tasks\n"
|
||||
"- After you're done with your tasks, respond to the supervisor directly\n"
|
||||
"- Respond ONLY with the results of your work, do NOT include ANY other text."
|
||||
),
|
||||
name="math_agent",
|
||||
)
|
||||
```
|
||||
|
||||
Let's run the math agent:
|
||||
|
||||
```python
|
||||
for chunk in math_agent.stream(
|
||||
{"messages": [{"role": "user", "content": "what's (3 + 5) x 7"}]}
|
||||
):
|
||||
pretty_print_messages(chunk)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: math_agent
|
||||
Tool Calls:
|
||||
add (call_p6OVLDHB4LyCNCxPOZzWR15v)
|
||||
Call ID: call_p6OVLDHB4LyCNCxPOZzWR15v
|
||||
Args:
|
||||
a: 3
|
||||
b: 5
|
||||
|
||||
|
||||
Update from node tools:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: add
|
||||
|
||||
8.0
|
||||
|
||||
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: math_agent
|
||||
Tool Calls:
|
||||
multiply (call_EoaWHMLFZAX4AkajQCtZvbli)
|
||||
Call ID: call_EoaWHMLFZAX4AkajQCtZvbli
|
||||
Args:
|
||||
a: 8
|
||||
b: 7
|
||||
|
||||
|
||||
Update from node tools:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: multiply
|
||||
|
||||
56.0
|
||||
|
||||
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: math_agent
|
||||
|
||||
56
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 2. Create supervisor with `langgraph-supervisor`
|
||||
|
||||
To implement out multi-agent system, we will use [`create_supervisor`][langgraph_supervisor.supervisor.create_supervisor] from the prebuilt `langgraph-supervisor` library:
|
||||
|
||||
```python
|
||||
from langgraph_supervisor import create_supervisor
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
supervisor = create_supervisor(
|
||||
model=init_chat_model("openai:gpt-4.1"),
|
||||
agents=[research_agent, math_agent],
|
||||
prompt=(
|
||||
"You are a supervisor managing two agents:\n"
|
||||
"- a research agent. Assign research-related tasks to this agent\n"
|
||||
"- a math agent. Assign math-related tasks to this agent\n"
|
||||
"Assign work to one agent at a time, do not call agents in parallel.\n"
|
||||
"Do not do any work yourself."
|
||||
),
|
||||
add_handoff_back_messages=True,
|
||||
output_mode="full_history",
|
||||
).compile()
|
||||
```
|
||||
|
||||
```python
|
||||
from IPython.display import display, Image
|
||||
|
||||
display(Image(supervisor.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Note:** When you run this code, it will generate and display a visual representation of the supervisor graph showing the flow between the supervisor and worker agents.
|
||||
|
||||
Let's now run it with a query that requires both agents:
|
||||
|
||||
* research agent will look up the necessary GDP information
|
||||
* math agent will perform division to find the percentage of NY state GDP, as requested
|
||||
|
||||
```python
|
||||
for chunk in supervisor.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "find US and New York state GDP in 2024. what % of US GDP was New York state?",
|
||||
}
|
||||
]
|
||||
},
|
||||
):
|
||||
pretty_print_messages(chunk, last_message=True)
|
||||
|
||||
final_message_history = chunk["supervisor"]["messages"]
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_to_research_agent
|
||||
|
||||
Successfully transferred to research_agent
|
||||
|
||||
|
||||
Update from node research_agent:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_back_to_supervisor
|
||||
|
||||
Successfully transferred back to supervisor
|
||||
|
||||
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_to_math_agent
|
||||
|
||||
Successfully transferred to math_agent
|
||||
|
||||
|
||||
Update from node math_agent:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_back_to_supervisor
|
||||
|
||||
Successfully transferred back to supervisor
|
||||
|
||||
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: supervisor
|
||||
|
||||
In 2024, the US GDP was $29.18 trillion and New York State's GDP was $2.297 trillion. New York State accounted for approximately 7.87% of the total US GDP in 2024.
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 3. Create supervisor from scratch
|
||||
|
||||
Let's now implement this same multi-agent system from scratch. We will need to:
|
||||
|
||||
1. [Set up how the supervisor communicates](#set-up-agent-communication) with individual agents
|
||||
2. [Create the supervisor agent](#create-supervisor-agent)
|
||||
3. Combine supervisor and worker agents into a [single multi-agent graph](#create-multi-agent-graph).
|
||||
|
||||
### Set up agent communication
|
||||
|
||||
We will need to define a way for the supervisor agent to communicate with the worker agents. A common way to implement this in multi-agent architectures is using **handoffs**, where one agent *hands off* control to another. Handoffs allow you to specify:
|
||||
|
||||
- **destination**: target agent to transfer to
|
||||
- **payload**: information to pass to that agent
|
||||
|
||||
We will implement handoffs via **handoff tools** and give these tools to the supervisor agent: when the supervisor calls these tools, it will hand off control to a worker agent, passing the full message history to that agent.
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
from langgraph.prebuilt import InjectedState
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
from langgraph.types import Command
|
||||
|
||||
|
||||
def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
name = f"transfer_to_{agent_name}"
|
||||
description = description or f"Ask {agent_name} for help."
|
||||
|
||||
@tool(name, description=description)
|
||||
def handoff_tool(
|
||||
state: Annotated[MessagesState, InjectedState],
|
||||
tool_call_id: Annotated[str, InjectedToolCallId],
|
||||
) -> Command:
|
||||
tool_message = {
|
||||
"role": "tool",
|
||||
"content": f"Successfully transferred to {agent_name}",
|
||||
"name": name,
|
||||
"tool_call_id": tool_call_id,
|
||||
}
|
||||
# highlight-next-line
|
||||
return Command(
|
||||
# highlight-next-line
|
||||
goto=agent_name, # (1)!
|
||||
# highlight-next-line
|
||||
update={**state, "messages": state["messages"] + [tool_message]}, # (2)!
|
||||
# highlight-next-line
|
||||
graph=Command.PARENT, # (3)!
|
||||
)
|
||||
|
||||
return handoff_tool
|
||||
|
||||
|
||||
# Handoffs
|
||||
assign_to_research_agent = create_handoff_tool(
|
||||
agent_name="research_agent",
|
||||
description="Assign task to a researcher agent.",
|
||||
)
|
||||
|
||||
assign_to_math_agent = create_handoff_tool(
|
||||
agent_name="math_agent",
|
||||
description="Assign task to a math agent.",
|
||||
)
|
||||
```
|
||||
|
||||
1. Name of the agent or node to hand off to.
|
||||
2. Take the agent's messages and add them to the parent's state as part of the handoff. The next agent will see the parent state.
|
||||
3. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
|
||||
### Create supervisor agent
|
||||
|
||||
Then, let's create the supervisor agent with the handoff tools we just defined. We will use the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent]:
|
||||
|
||||
```python
|
||||
supervisor_agent = create_react_agent(
|
||||
model="openai:gpt-4.1",
|
||||
tools=[assign_to_research_agent, assign_to_math_agent],
|
||||
prompt=(
|
||||
"You are a supervisor managing two agents:\n"
|
||||
"- a research agent. Assign research-related tasks to this agent\n"
|
||||
"- a math agent. Assign math-related tasks to this agent\n"
|
||||
"Assign work to one agent at a time, do not call agents in parallel.\n"
|
||||
"Do not do any work yourself."
|
||||
),
|
||||
name="supervisor",
|
||||
)
|
||||
```
|
||||
|
||||
### Create multi-agent graph
|
||||
|
||||
Putting this all together, let's create a graph for our overall multi-agent system. We will add the supervisor and the individual agents as subgraph [nodes](../../concepts/low_level.md#nodes).
|
||||
|
||||
```python
|
||||
from langgraph.graph import END
|
||||
|
||||
# Define the multi-agent supervisor graph
|
||||
supervisor = (
|
||||
StateGraph(MessagesState)
|
||||
# NOTE: `destinations` is only needed for visualization and doesn't affect runtime behavior
|
||||
.add_node(supervisor_agent, destinations=("research_agent", "math_agent", END))
|
||||
.add_node(research_agent)
|
||||
.add_node(math_agent)
|
||||
.add_edge(START, "supervisor")
|
||||
# always return back to the supervisor
|
||||
.add_edge("research_agent", "supervisor")
|
||||
.add_edge("math_agent", "supervisor")
|
||||
.compile()
|
||||
)
|
||||
```
|
||||
|
||||
Notice that we've added explicit [edges](../../concepts/low_level.md#edges) from worker agents back to the supervisor — this means that they are guaranteed to return control back to the supervisor. If you want the agents to respond directly to the user (i.e., turn the system into a router, you can remove these edges).
|
||||
|
||||
```python
|
||||
from IPython.display import display, Image
|
||||
|
||||
display(Image(supervisor.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Note:** When you run this code, it will generate and display a visual representation of the multi-agent supervisor graph showing the flow between the supervisor and worker agents.
|
||||
|
||||
With the multi-agent graph created, let's now run it!
|
||||
|
||||
```python
|
||||
for chunk in supervisor.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "find US and New York state GDP in 2024. what % of US GDP was New York state?",
|
||||
}
|
||||
]
|
||||
},
|
||||
):
|
||||
pretty_print_messages(chunk, last_message=True)
|
||||
|
||||
final_message_history = chunk["supervisor"]["messages"]
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_to_research_agent
|
||||
|
||||
Successfully transferred to research_agent
|
||||
|
||||
|
||||
Update from node research_agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: research_agent
|
||||
|
||||
- US GDP in 2024 is projected to be about $28.18 trillion USD (Statista; CBO projection).
|
||||
- New York State's nominal GDP for 2024 is estimated at approximately $2.16 trillion USD (various economic reports).
|
||||
- New York State's share of US GDP in 2024 is roughly 7.7%.
|
||||
|
||||
Sources:
|
||||
- https://www.statista.com/statistics/216985/forecast-of-us-gross-domestic-product/
|
||||
- https://nyassembly.gov/Reports/WAM/2025economic_revenue/2025_report.pdf?v=1740533306
|
||||
|
||||
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_to_math_agent
|
||||
|
||||
Successfully transferred to math_agent
|
||||
|
||||
|
||||
Update from node math_agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: math_agent
|
||||
|
||||
US GDP in 2024: $28.18 trillion
|
||||
New York State GDP in 2024: $2.16 trillion
|
||||
Percentage of US GDP from New York State: 7.67%
|
||||
|
||||
|
||||
Update from node supervisor:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: supervisor
|
||||
|
||||
Here are your results:
|
||||
|
||||
- 2024 US GDP (projected): $28.18 trillion USD
|
||||
- 2024 New York State GDP (estimated): $2.16 trillion USD
|
||||
- New York State's share of US GDP: approximately 7.7%
|
||||
|
||||
If you need the calculation steps or sources, let me know!
|
||||
|
||||
|
||||
```
|
||||
|
||||
Let's examine the full resulting message history:
|
||||
|
||||
```python
|
||||
for message in final_message_history:
|
||||
message.pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================ Human Message ==================================
|
||||
|
||||
find US and New York state GDP in 2024. what % of US GDP was New York state?
|
||||
================================== Ai Message ===================================
|
||||
Name: supervisor
|
||||
Tool Calls:
|
||||
transfer_to_research_agent (call_KlGgvF5ahlAbjX8d2kHFjsC3)
|
||||
Call ID: call_KlGgvF5ahlAbjX8d2kHFjsC3
|
||||
Args:
|
||||
================================= Tool Message ==================================
|
||||
Name: transfer_to_research_agent
|
||||
|
||||
Successfully transferred to research_agent
|
||||
================================== Ai Message ===================================
|
||||
Name: research_agent
|
||||
Tool Calls:
|
||||
tavily_search (call_ZOaTVUA6DKrOjWQldLhtrsO2)
|
||||
Call ID: call_ZOaTVUA6DKrOjWQldLhtrsO2
|
||||
Args:
|
||||
query: US GDP 2024 estimate or actual
|
||||
search_depth: advanced
|
||||
tavily_search (call_QsRAasxW9K03lTlqjuhNLFbZ)
|
||||
Call ID: call_QsRAasxW9K03lTlqjuhNLFbZ
|
||||
Args:
|
||||
query: New York state GDP 2024 estimate or actual
|
||||
search_depth: advanced
|
||||
================================= Tool Message ==================================
|
||||
Name: tavily_search
|
||||
|
||||
{"query": "US GDP 2024 estimate or actual", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.advisorperspectives.com/dshort/updates/2025/05/29/gdp-gross-domestic-product-q1-2025-second-estimate", "title": "Q1 GDP Second Estimate: Real GDP at -0.2%, Higher Than Expected", "content": "> Real gross domestic product (GDP) decreased at an annual rate of 0.2 percent in the first quarter of 2025 (January, February, and March), according to the second estimate released by the U.S. Bureau of Economic Analysis. In the fourth quarter of 2024, real GDP increased 2.4 percent. The decrease in real GDP in the first quarter primarily reflected an increase in imports, which are a subtraction in the calculation of GDP, and a decrease in government spending. These movements were partly [...] by [Harry Mamaysky](https://www.advisor```
|
||||
```
|
||||
|
||||
!!! important
|
||||
You can see that the supervisor system appends **all** of the individual agent messages (i.e., their internal tool-calling loop) to the full message history. This means that on every supervisor turn, supervisor agent sees this full history. If you want more control over:
|
||||
|
||||
* **how inputs are passed to agents**: you can use LangGraph [`Send()`][langgraph.types.Send] primitive to directly send data to the worker agents during the handoff. See the [task delegation](#4-create-delegation-tasks) example below
|
||||
* **how agent outputs are added**: you can control how much of the agent's internal message history is added to the overall supervisor message history by wrapping the agent in a separate node function:
|
||||
|
||||
```python
|
||||
def call_research_agent(state):
|
||||
# return agent's final response,
|
||||
# excluding inner monologue
|
||||
response = research_agent.invoke(state)
|
||||
# highlight-next-line
|
||||
return {"messages": response["messages"][-1]}
|
||||
```
|
||||
|
||||
## 4. Create delegation tasks
|
||||
|
||||
So far the individual agents relied on **interpreting full message history** to determine their tasks. An alternative approach is to ask the supervisor to **formulate a task explicitly**. We can do so by adding a `task_description` parameter to the `handoff_tool` function.
|
||||
|
||||
```python
|
||||
from langgraph.types import Send
|
||||
|
||||
|
||||
def create_task_description_handoff_tool(
|
||||
*, agent_name: str, description: str | None = None
|
||||
):
|
||||
name = f"transfer_to_{agent_name}"
|
||||
description = description or f"Ask {agent_name} for help."
|
||||
|
||||
@tool(name, description=description)
|
||||
def handoff_tool(
|
||||
# this is populated by the supervisor LLM
|
||||
task_description: Annotated[
|
||||
str,
|
||||
"Description of what the next agent should do, including all of the relevant context.",
|
||||
],
|
||||
# these parameters are ignored by the LLM
|
||||
state: Annotated[MessagesState, InjectedState],
|
||||
) -> Command:
|
||||
task_description_message = {"role": "user", "content": task_description}
|
||||
agent_input = {**state, "messages": [task_description_message]}
|
||||
return Command(
|
||||
# highlight-next-line
|
||||
goto=[Send(agent_name, agent_input)],
|
||||
graph=Command.PARENT,
|
||||
)
|
||||
|
||||
return handoff_tool
|
||||
|
||||
|
||||
assign_to_research_agent_with_description = create_task_description_handoff_tool(
|
||||
agent_name="research_agent",
|
||||
description="Assign task to a researcher agent.",
|
||||
)
|
||||
|
||||
assign_to_math_agent_with_description = create_task_description_handoff_tool(
|
||||
agent_name="math_agent",
|
||||
description="Assign task to a math agent.",
|
||||
)
|
||||
|
||||
supervisor_agent_with_description = create_react_agent(
|
||||
model="openai:gpt-4.1",
|
||||
tools=[
|
||||
assign_to_research_agent_with_description,
|
||||
assign_to_math_agent_with_description,
|
||||
],
|
||||
prompt=(
|
||||
"You are a supervisor managing two agents:\n"
|
||||
"- a research agent. Assign research-related tasks to this assistant\n"
|
||||
"- a math agent. Assign math-related tasks to this assistant\n"
|
||||
"Assign work to one agent at a time, do not call agents in parallel.\n"
|
||||
"Do not do any work yourself."
|
||||
),
|
||||
name="supervisor",
|
||||
)
|
||||
|
||||
supervisor_with_description = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(
|
||||
supervisor_agent_with_description, destinations=("research_agent", "math_agent")
|
||||
)
|
||||
.add_node(research_agent)
|
||||
.add_node(math_agent)
|
||||
.add_edge(START, "supervisor")
|
||||
.add_edge("research_agent", "supervisor")
|
||||
.add_edge("math_agent", "supervisor")
|
||||
.compile()
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
We're using [`Send()`][langgraph.types.Send] primitive in the `handoff_tool`. This means that instead of receiving the full `supervisor` graph state as input, each worker agent only sees the contents of the `Send` payload. In this example, we're sending the task description as a single "human" message.
|
||||
|
||||
Let's now running it with the same input query:
|
||||
|
||||
```python
|
||||
for chunk in supervisor_with_description.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "find US and New York state GDP in 2024. what % of US GDP was New York state?",
|
||||
}
|
||||
]
|
||||
},
|
||||
subgraphs=True,
|
||||
):
|
||||
pretty_print_messages(chunk, last_message=True)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from subgraph supervisor:
|
||||
|
||||
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: supervisor
|
||||
Tool Calls:
|
||||
transfer_to_research_agent (call_tk8q8py8qK6MQz6Kj6mijKua)
|
||||
Call ID: call_tk8q8py8qK6MQz6Kj6mijKua
|
||||
Args:
|
||||
task_description: Find the 2024 GDP (Gross Domestic Product) for both the United States and New York state, using the most up-to-date and reputable sources available. Provide both GDP values and cite the data sources.
|
||||
|
||||
|
||||
Update from subgraph research_agent:
|
||||
|
||||
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: research_agent
|
||||
Tool Calls:
|
||||
tavily_search (call_KqvhSvOIhAvXNsT6BOwbPlRB)
|
||||
Call ID: call_KqvhSvOIhAvXNsT6BOwbPlRB
|
||||
Args:
|
||||
query: 2024 United States GDP value from a reputable source
|
||||
search_depth: advanced
|
||||
tavily_search (call_kbbAWBc9KwCWKHmM5v04H88t)
|
||||
Call ID: call_kbbAWBc9KwCWKHmM5v04H88t
|
||||
Args:
|
||||
query: 2024 New York state GDP value from a reputable source
|
||||
search_depth: advanced
|
||||
|
||||
|
||||
Update from subgraph research_agent:
|
||||
|
||||
|
||||
Update from node tools:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: tavily_search
|
||||
|
||||
{"query": "2024 United States GDP value from a reputable source", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.focus-economics.com/countries/united-states/", "title": "United States Economy Overview - Focus Economics", "content": "The United States' Macroeconomic Analysis:\n------------------------------------------\n\n**Nominal GDP of USD 29,185 billion in 2024.**\n\n**Nominal GDP of USD 29,179 billion in 2024.**\n\n**GDP per capita of USD 86,635 compared to the global average of USD 10,589.**\n\n**GDP per capita of USD 86,652 compared to the global average of USD 10,589.**\n\n**Average real GDP growth of 2.5% over the last decade.**\n\n**Average real GDP growth of ```
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,21 +0,0 @@
|
||||
# Examples
|
||||
|
||||
The pages in this section provide end-to-end examples for the following topics:
|
||||
|
||||
## General
|
||||
|
||||
- [Agentic RAG](./rag/langgraph_adaptive_rag.ipynb)
|
||||
- [Agent Supervisor](./multi_agent/agent_supervisor.ipynb)
|
||||
- [SQL agent](./sql-agent.ipynb)
|
||||
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.ipynb)
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
- [Set up custom authentication](./auth/getting_started.md)
|
||||
- [Make conversations private](./auth/resource_auth.md)
|
||||
- [Connect an authentication provider](./auth/add_auth_server.md)
|
||||
- [Rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
|
||||
- [Use RemoteGraph](../how-tos/use-remote-graph.md)
|
||||
- [Deploy CrewAI, AutoGen, and other frameworks](../how-tos/autogen-langgraph-platform.ipynb)
|
||||
- [Integrate LangGraph into a React app](../cloud/how-tos/use_stream_react.md)
|
||||
- [Implement Generative User Interfaces with LangGraph](../cloud/how-tos/generative_ui_react.md)
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -0,0 +1,522 @@
|
||||
# Agentic RAG
|
||||
|
||||
In this tutorial we will build a [retrieval agent](https://python.langchain.com/docs/tutorials/qa_chat_history). Retrieval agents are useful when you want an LLM to make a decision about whether to retrieve context from a vectorstore or respond to the user directly.
|
||||
|
||||
By the end of the tutorial we will have done the following:
|
||||
|
||||
1. Fetch and preprocess documents that will be used for retrieval.
|
||||
2. Index those documents for semantic search and create a retriever tool for the agent.
|
||||
3. Build an agentic RAG system that can decide when to use the retriever tool.
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
Let's download the required packages and set our API keys:
|
||||
|
||||
```python
|
||||
%%capture --no-stderr
|
||||
%pip install -U --quiet langgraph "langchain[openai]" langchain-community langchain-text-splitters
|
||||
```
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
|
||||
def _set_env(key: str):
|
||||
if key not in os.environ:
|
||||
os.environ[key] = getpass.getpass(f"{key}:")
|
||||
|
||||
|
||||
_set_env("OPENAI_API_KEY")
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph.
|
||||
|
||||
|
||||
## 1. Preprocess documents
|
||||
|
||||
1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages using `WebBaseLoader` utility:
|
||||
|
||||
```python
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
|
||||
urls = [
|
||||
"https://lilianweng.github.io/posts/2024-11-28-reward-hacking/",
|
||||
"https://lilianweng.github.io/posts/2024-07-07-hallucination/",
|
||||
"https://lilianweng.github.io/posts/2024-04-12-diffusion-video/",
|
||||
]
|
||||
|
||||
docs = [WebBaseLoader(url).load() for url in urls]
|
||||
```
|
||||
|
||||
```python
|
||||
docs[0][0].page_content.strip()[:1000]
|
||||
```
|
||||
|
||||
2. Split the fetched documents into smaller chunks for indexing into our vectorstore:
|
||||
|
||||
```python
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
|
||||
docs_list = [item for sublist in docs for item in sublist]
|
||||
|
||||
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
||||
chunk_size=100, chunk_overlap=50
|
||||
)
|
||||
doc_splits = text_splitter.split_documents(docs_list)
|
||||
```
|
||||
|
||||
```python
|
||||
doc_splits[0].page_content.strip()
|
||||
```
|
||||
|
||||
## 2. Create a retriever tool
|
||||
|
||||
Now that we have our split documents, we can index them into a vector store that we'll use for semantic search.
|
||||
|
||||
1. Use an in-memory vector store and OpenAI embeddings:
|
||||
|
||||
```python
|
||||
from langchain_core.vectorstores import InMemoryVectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
|
||||
vectorstore = InMemoryVectorStore.from_documents(
|
||||
documents=doc_splits, embedding=OpenAIEmbeddings()
|
||||
)
|
||||
retriever = vectorstore.as_retriever()
|
||||
```
|
||||
|
||||
2. Create a retriever tool using LangChain's prebuilt `create_retriever_tool`:
|
||||
|
||||
```python
|
||||
from langchain.tools.retriever import create_retriever_tool
|
||||
|
||||
retriever_tool = create_retriever_tool(
|
||||
retriever,
|
||||
"retrieve_blog_posts",
|
||||
"Search and return information about Lilian Weng blog posts.",
|
||||
)
|
||||
```
|
||||
|
||||
3. Test the tool:
|
||||
|
||||
```python
|
||||
retriever_tool.invoke({"query": "types of reward hacking"})
|
||||
```
|
||||
|
||||
## 3. Generate query
|
||||
|
||||
Now we will start building components ([nodes](../../concepts/low_level.md#nodes) and [edges](../../concepts/low_level.md#edges)) for our agentic RAG graph. Note that the components will operate on the [`MessagesState`](../../concepts/low_level.md#messagesstate) — graph state that contains a `messages` key with a list of [chat messages](https://python.langchain.com/docs/concepts/messages/).
|
||||
|
||||
1. Build a `generate_query_or_respond` node. It will call an LLM to generate a response based on the current graph state (list of messages). Given the input messages, it will decide to retrieve using the retriever tool, or respond directly to the user. Note that we're giving the chat model access to the `retriever_tool` we created earlier via `.bind_tools`:
|
||||
|
||||
```python
|
||||
from langgraph.graph import MessagesState
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
response_model = init_chat_model("openai:gpt-4.1", temperature=0)
|
||||
|
||||
|
||||
def generate_query_or_respond(state: MessagesState):
|
||||
"""Call the model to generate a response based on the current state. Given
|
||||
the question, it will decide to retrieve using the retriever tool, or simply respond to the user.
|
||||
"""
|
||||
response = (
|
||||
response_model
|
||||
# highlight-next-line
|
||||
.bind_tools([retriever_tool]).invoke(state["messages"])
|
||||
)
|
||||
return {"messages": [response]}
|
||||
```
|
||||
|
||||
2. Try it on a random input:
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "user", "content": "hello!"}]}
|
||||
generate_query_or_respond(input)["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Hello! How can I help you today?
|
||||
```
|
||||
|
||||
3. Ask a question that requires semantic search:
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
}
|
||||
]
|
||||
}
|
||||
generate_query_or_respond(input)["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
retrieve_blog_posts (call_tYQxgfIlnQUDMdtAhdbXNwIM)
|
||||
Call ID: call_tYQxgfIlnQUDMdtAhdbXNwIM
|
||||
Args:
|
||||
query: types of reward hacking
|
||||
```
|
||||
|
||||
## 4. Grade documents
|
||||
|
||||
1. Add a [conditional edge](../../concepts/low_level.md#conditional-edges) — `grade_documents` — to determine whether the retrieved documents are relevant to the question. We will use a model with a structured output schema `GradeDocuments` for document grading. The `grade_documents` function will return the name of the node to go to based on the grading decision (`generate_answer` or `rewrite_question`):
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
|
||||
GRADE_PROMPT = (
|
||||
"You are a grader assessing relevance of a retrieved document to a user question. \n "
|
||||
"Here is the retrieved document: \n\n {context} \n\n"
|
||||
"Here is the user question: {question} \n"
|
||||
"If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n"
|
||||
"Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question."
|
||||
)
|
||||
|
||||
|
||||
# highlight-next-line
|
||||
class GradeDocuments(BaseModel):
|
||||
"""Grade documents using a binary score for relevance check."""
|
||||
|
||||
binary_score: str = Field(
|
||||
description="Relevance score: 'yes' if relevant, or 'no' if not relevant"
|
||||
)
|
||||
|
||||
|
||||
grader_model = init_chat_model("openai:gpt-4.1", temperature=0)
|
||||
|
||||
|
||||
def grade_documents(
|
||||
state: MessagesState,
|
||||
) -> Literal["generate_answer", "rewrite_question"]:
|
||||
"""Determine whether the retrieved documents are relevant to the question."""
|
||||
question = state["messages"][0].content
|
||||
context = state["messages"][-1].content
|
||||
|
||||
prompt = GRADE_PROMPT.format(question=question, context=context)
|
||||
response = (
|
||||
grader_model
|
||||
# highlight-next-line
|
||||
.with_structured_output(GradeDocuments).invoke(
|
||||
[{"role": "user", "content": prompt}]
|
||||
)
|
||||
)
|
||||
score = response.binary_score
|
||||
|
||||
if score == "yes":
|
||||
return "generate_answer"
|
||||
else:
|
||||
return "rewrite_question"
|
||||
```
|
||||
|
||||
2. Run this with irrelevant documents in the tool response:
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
|
||||
input = {
|
||||
"messages": convert_to_messages(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "retrieve_blog_posts",
|
||||
"args": {"query": "types of reward hacking"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "content": "meow", "tool_call_id": "1"},
|
||||
]
|
||||
)
|
||||
}
|
||||
grade_documents(input)
|
||||
```
|
||||
|
||||
3. Confirm that the relevant documents are classified as such:
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": convert_to_messages(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "retrieve_blog_posts",
|
||||
"args": {"query": "types of reward hacking"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering",
|
||||
"tool_call_id": "1",
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
grade_documents(input)
|
||||
```
|
||||
|
||||
## 5. Rewrite question
|
||||
|
||||
1. Build the `rewrite_question` node. The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question. To do so, we will call the `rewrite_question` node:
|
||||
|
||||
```python
|
||||
REWRITE_PROMPT = (
|
||||
"Look at the input and try to reason about the underlying semantic intent / meaning.\n"
|
||||
"Here is the initial question:"
|
||||
"\n ------- \n"
|
||||
"{question}"
|
||||
"\n ------- \n"
|
||||
"Formulate an improved question:"
|
||||
)
|
||||
|
||||
|
||||
def rewrite_question(state: MessagesState):
|
||||
"""Rewrite the original user question."""
|
||||
messages = state["messages"]
|
||||
question = messages[0].content
|
||||
prompt = REWRITE_PROMPT.format(question=question)
|
||||
response = response_model.invoke([{"role": "user", "content": prompt}])
|
||||
return {"messages": [{"role": "user", "content": response.content}]}
|
||||
```
|
||||
|
||||
2. Try it out:
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": convert_to_messages(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "retrieve_blog_posts",
|
||||
"args": {"query": "types of reward hacking"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "content": "meow", "tool_call_id": "1"},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
response = rewrite_question(input)
|
||||
print(response["messages"][-1]["content"])
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
What are the different types of reward hacking described by Lilian Weng, and how does she explain them?
|
||||
```
|
||||
|
||||
## 6. Generate an answer
|
||||
|
||||
1. Build `generate_answer` node: if we pass the grader checks, we can generate the final answer based on the original question and the retrieved context:
|
||||
|
||||
```python
|
||||
GENERATE_PROMPT = (
|
||||
"You are an assistant for question-answering tasks. "
|
||||
"Use the following pieces of retrieved context to answer the question. "
|
||||
"If you don't know the answer, just say that you don't know. "
|
||||
"Use three sentences maximum and keep the answer concise.\n"
|
||||
"Question: {question} \n"
|
||||
"Context: {context}"
|
||||
)
|
||||
|
||||
|
||||
def generate_answer(state: MessagesState):
|
||||
"""Generate an answer."""
|
||||
question = state["messages"][0].content
|
||||
context = state["messages"][-1].content
|
||||
prompt = GENERATE_PROMPT.format(question=question, context=context)
|
||||
response = response_model.invoke([{"role": "user", "content": prompt}])
|
||||
return {"messages": [response]}
|
||||
```
|
||||
|
||||
2. Try it:
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": convert_to_messages(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "retrieve_blog_posts",
|
||||
"args": {"query": "types of reward hacking"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering",
|
||||
"tool_call_id": "1",
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
response = generate_answer(input)
|
||||
response["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.
|
||||
```
|
||||
|
||||
## 7. Assemble the graph
|
||||
|
||||
* Start with a `generate_query_or_respond` and determine if we need to call `retriever_tool`
|
||||
* Route to next step using `tools_condition`:
|
||||
* If `generate_query_or_respond` returned `tool_calls`, call `retriever_tool` to retrieve context
|
||||
* Otherwise, respond directly to the user
|
||||
* Grade retrieved document content for relevance to the question (`grade_documents`) and route to next step:
|
||||
* If not relevant, rewrite the question using `rewrite_question` and then call `generate_query_or_respond` again
|
||||
* If relevant, proceed to `generate_answer` and generate final response using the `ToolMessage` with the retrieved document context
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.prebuilt import tools_condition
|
||||
|
||||
workflow = StateGraph(MessagesState)
|
||||
|
||||
# Define the nodes we will cycle between
|
||||
workflow.add_node(generate_query_or_respond)
|
||||
workflow.add_node("retrieve", ToolNode([retriever_tool]))
|
||||
workflow.add_node(rewrite_question)
|
||||
workflow.add_node(generate_answer)
|
||||
|
||||
workflow.add_edge(START, "generate_query_or_respond")
|
||||
|
||||
# Decide whether to retrieve
|
||||
workflow.add_conditional_edges(
|
||||
"generate_query_or_respond",
|
||||
# Assess LLM decision (call `retriever_tool` tool or respond to the user)
|
||||
tools_condition,
|
||||
{
|
||||
# Translate the condition outputs to nodes in our graph
|
||||
"tools": "retrieve",
|
||||
END: END,
|
||||
},
|
||||
)
|
||||
|
||||
# Edges taken after the `action` node is called.
|
||||
workflow.add_conditional_edges(
|
||||
"retrieve",
|
||||
# Assess agent decision
|
||||
grade_documents,
|
||||
)
|
||||
workflow.add_edge("generate_answer", END)
|
||||
workflow.add_edge("rewrite_question", "generate_query_or_respond")
|
||||
|
||||
# Compile
|
||||
graph = workflow.compile()
|
||||
```
|
||||
|
||||
Visualize the graph:
|
||||
|
||||
```python
|
||||
from IPython.display import Image, display
|
||||
|
||||
display(Image(graph.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 8. Run the agentic RAG
|
||||
|
||||
```python
|
||||
for chunk in graph.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What does Lilian Weng say about types of reward hacking?",
|
||||
}
|
||||
]
|
||||
}
|
||||
):
|
||||
for node, update in chunk.items():
|
||||
print("Update from node", node)
|
||||
update["messages"][-1].pretty_print()
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node generate_query_or_respond
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
retrieve_blog_posts (call_NYu2vq4km9nNNEFqJwefWKu1)
|
||||
Call ID: call_NYu2vq4km9nNNEFqJwefWKu1
|
||||
Args:
|
||||
query: types of reward hacking
|
||||
|
||||
|
||||
|
||||
Update from node retrieve
|
||||
================================= Tool Message ==================================
|
||||
Name: retrieve_blog_posts
|
||||
|
||||
(Note: Some work defines reward tampering as a distinct category of misalignment behavior from reward hacking. But I consider reward hacking as a broader concept here.)
|
||||
At a high level, reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering.
|
||||
|
||||
Why does Reward Hacking Exist?#
|
||||
|
||||
Pan et al. (2022) investigated reward hacking as a function of agent capabilities, including (1) model size, (2) action space resolution, (3) observation space noise, and (4) training time. They also proposed a taxonomy of three types of misspecified proxy rewards:
|
||||
|
||||
Let's Define Reward Hacking#
|
||||
Reward shaping in RL is challenging. Reward hacking occurs when an RL agent exploits flaws or ambiguities in the reward function to obtain high rewards without genuinely learning the intended behaviors or completing the task as designed. In recent years, several related concepts have been proposed, all referring to some form of reward hacking:
|
||||
|
||||
|
||||
|
||||
Update from node generate_answer
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.
|
||||
```
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,549 @@
|
||||
# Build a SQL agent
|
||||
|
||||
In this tutorial, we will walk through how to build an agent that can answer questions about a SQL database.
|
||||
|
||||
At a high level, the agent will:
|
||||
|
||||
1. Fetch the available tables from the database
|
||||
2. Decide which tables are relevant to the question
|
||||
3. Fetch the schemas for the relevant tables
|
||||
4. Generate a query based on the question and information from the schemas
|
||||
5. Double-check the query for common mistakes using an LLM
|
||||
6. Execute the query and return the results
|
||||
7. Correct mistakes surfaced by the database engine until the query is successful
|
||||
8. Formulate a response based on the results
|
||||
|
||||
!!! warning "Security note"
|
||||
Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your agent's needs. This will mitigate though not eliminate the risks of building a model-driven system.
|
||||
|
||||
## 1. Setup
|
||||
|
||||
Let's first install some dependencies. This tutorial uses SQL database and tool abstractions from [langchain-community](https://python.langchain.com/docs/concepts/architecture/#langchain-community). We will also require a LangChain [chat model](https://python.langchain.com/docs/concepts/chat_models/).
|
||||
|
||||
```python
|
||||
%%capture --no-stderr
|
||||
%pip install -U langgraph langchain_community "langchain[openai]"
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph.
|
||||
|
||||
### Select a LLM
|
||||
|
||||
First we [initialize our LLM](https://python.langchain.com/docs/how_to/chat_models_universal_init/). Any model supporting [tool-calling](https://python.langchain.com/docs/integrations/chat/#featured-providers) should work. We use OpenAI below.
|
||||
|
||||
```python
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
llm = init_chat_model("openai:gpt-4.1")
|
||||
```
|
||||
|
||||
### Configure the database
|
||||
|
||||
We will be creating a SQLite database for this tutorial. SQLite is a lightweight database that is easy to set up and use. We will be loading the `chinook` database, which is a sample database that represents a digital media store.
|
||||
Find more information about the database [here](https://www.sqlitetutorial.net/sqlite-sample-database/).
|
||||
|
||||
For convenience, we have hosted the database (`Chinook.db`) on a public GCS bucket.
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db"
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Open a local file in binary write mode
|
||||
with open("Chinook.db", "wb") as file:
|
||||
# Write the content of the response (the file) to the local file
|
||||
file.write(response.content)
|
||||
print("File downloaded and saved as Chinook.db")
|
||||
else:
|
||||
print(f"Failed to download the file. Status code: {response.status_code}")
|
||||
```
|
||||
|
||||
We will use a handy SQL database wrapper available in the `langchain_community` package to interact with the database. The wrapper provides a simple interface to execute SQL queries and fetch results:
|
||||
|
||||
```python
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
|
||||
db = SQLDatabase.from_uri("sqlite:///Chinook.db")
|
||||
|
||||
print(f"Dialect: {db.dialect}")
|
||||
print(f"Available tables: {db.get_usable_table_names()}")
|
||||
print(f'Sample output: {db.run("SELECT * FROM Artist LIMIT 5;")}')
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Dialect: sqlite
|
||||
Available tables: ['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']
|
||||
Sample output: [(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains')]
|
||||
```
|
||||
|
||||
### Tools for database interactions
|
||||
|
||||
`langchain-community` implements some built-in tools for interacting with our `SQLDatabase`, including tools for listing tables, reading table schemas, and checking and running queries:
|
||||
|
||||
```python
|
||||
from langchain_community.agent_toolkits import SQLDatabaseToolkit
|
||||
|
||||
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
||||
|
||||
tools = toolkit.get_tools()
|
||||
|
||||
for tool in tools:
|
||||
print(f"{tool.name}: {tool.description}\n")
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
sql_db_query: Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.
|
||||
|
||||
sql_db_schema: Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3
|
||||
|
||||
sql_db_list_tables: Input is an empty string, output is a comma-separated list of tables in the database.
|
||||
|
||||
sql_db_query_checker: Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!
|
||||
|
||||
```
|
||||
|
||||
## 2. Using a prebuilt agent
|
||||
|
||||
Given these tools, we can initialize a pre-built agent in a single line. To customize our agents behavior, we write a descriptive system prompt.
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
system_prompt = """
|
||||
You are an agent designed to interact with a SQL database.
|
||||
Given an input question, create a syntactically correct {dialect} query to run,
|
||||
then look at the results of the query and return the answer. Unless the user
|
||||
specifies a specific number of examples they wish to obtain, always limit your
|
||||
query to at most {top_k} results.
|
||||
|
||||
You can order the results by a relevant column to return the most interesting
|
||||
examples in the database. Never query for all the columns from a specific table,
|
||||
only ask for the relevant columns given the question.
|
||||
|
||||
You MUST double check your query before executing it. If you get an error while
|
||||
executing a query, rewrite the query and try again.
|
||||
|
||||
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the
|
||||
database.
|
||||
|
||||
To start you should ALWAYS look at the tables in the database to see what you
|
||||
can query. Do NOT skip this step.
|
||||
|
||||
Then you should query the schema of the most relevant tables.
|
||||
""".format(
|
||||
dialect=db.dialect,
|
||||
top_k=5,
|
||||
)
|
||||
|
||||
agent = create_react_agent(
|
||||
llm,
|
||||
tools,
|
||||
prompt=system_prompt,
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
This system prompt includes a number of instructions, such as always running specific tools before or after others. In the [next section](#3-customizing-the-agent), we will enforce these behaviors through the graph's structure, providing us a greater degree of control and allowing us to simplify the prompt.
|
||||
|
||||
Let's run this agent on a sample query and observe its behavior:
|
||||
|
||||
```python
|
||||
question = "Which genre on average has the longest tracks?"
|
||||
|
||||
for step in agent.stream(
|
||||
{"messages": [{"role": "user", "content": question}]},
|
||||
stream_mode="values",
|
||||
):
|
||||
step["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================ Human Message =================================
|
||||
|
||||
Which genre on average has the longest tracks?
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_list_tables (call_d8lCgywSroCgpVl558nmXKwA)
|
||||
Call ID: call_d8lCgywSroCgpVl558nmXKwA
|
||||
Args:
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_list_tables
|
||||
|
||||
Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_schema (call_nNf6IIUcwMYLIkE0l6uWkZHe)
|
||||
Call ID: call_nNf6IIUcwMYLIkE0l6uWkZHe
|
||||
Args:
|
||||
table_names: Genre, Track
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_schema
|
||||
|
||||
|
||||
CREATE TABLE "Genre" (
|
||||
"GenreId" INTEGER NOT NULL,
|
||||
"Name" NVARCHAR(120),
|
||||
PRIMARY KEY ("GenreId")
|
||||
)
|
||||
|
||||
/*
|
||||
3 rows from Genre table:
|
||||
GenreId Name
|
||||
1 Rock
|
||||
2 Jazz
|
||||
3 Metal
|
||||
*/
|
||||
|
||||
|
||||
CREATE TABLE "Track" (
|
||||
"TrackId" INTEGER NOT NULL,
|
||||
"Name" NVARCHAR(200) NOT NULL,
|
||||
"AlbumId" INTEGER,
|
||||
"MediaTypeId" INTEGER NOT NULL,
|
||||
"GenreId" INTEGER,
|
||||
"Composer" NVARCHAR(220),
|
||||
"Milliseconds" INTEGER NOT NULL,
|
||||
"Bytes" INTEGER,
|
||||
"UnitPrice" NUMERIC(10, 2) NOT NULL,
|
||||
PRIMARY KEY ("TrackId"),
|
||||
FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
|
||||
FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
|
||||
FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
|
||||
)
|
||||
|
||||
/*
|
||||
3 rows from Track table:
|
||||
TrackId Name AlbumId MediaTypeId GenreId Composer Milliseconds Bytes UnitPrice
|
||||
1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99
|
||||
2 Balls to the Wall 2 2 1 None 342562 5510424 0.99
|
||||
3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99
|
||||
*/
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_query_checker (call_urTRmtiGtTxkwHtscec7Fd2K)
|
||||
Call ID: call_urTRmtiGtTxkwHtscec7Fd2K
|
||||
Args:
|
||||
query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds
|
||||
FROM Track
|
||||
JOIN Genre ON Track.GenreId = Genre.GenreId
|
||||
GROUP BY Genre.Name
|
||||
ORDER BY AvgMilliseconds DESC
|
||||
LIMIT 1;
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_query_checker
|
||||
|
||||
\`\`\`sql
|
||||
SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds
|
||||
FROM Track
|
||||
JOIN Genre ON Track.GenreId = Genre.GenreId
|
||||
GROUP BY Genre.Name
|
||||
ORDER BY AvgMilliseconds DESC
|
||||
LIMIT 1;
|
||||
\`\`\`
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_query (call_RNMqyUEMv0rvy0UxSwrXY2AV)
|
||||
Call ID: call_RNMqyUEMv0rvy0UxSwrXY2AV
|
||||
Args:
|
||||
query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds
|
||||
FROM Track
|
||||
JOIN Genre ON Track.GenreId = Genre.GenreId
|
||||
GROUP BY Genre.Name
|
||||
ORDER BY AvgMilliseconds DESC
|
||||
LIMIT 1;
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_query
|
||||
|
||||
[('Sci Fi & Fantasy', 2911783.0384615385)]
|
||||
================================== Ai Message ==================================
|
||||
|
||||
The genre with the longest average track length is "Sci Fi & Fantasy," with an average duration of about 2,911,783 milliseconds (approximately 48.5 minutes) per track.
|
||||
```
|
||||
|
||||
This worked well enough: the agent correctly listed the tables, obtained the schemas, wrote a query, checked the query, and ran it to inform its final response.
|
||||
|
||||
!!! tip
|
||||
You can inspect all aspects of the above run, including steps taken, tools invoked, what prompts were seen by the LLM, and more in the [LangSmith trace](https://smith.langchain.com/public/bd594960-73e3-474b-b6f2-db039d7c713a/r).
|
||||
|
||||
## 3. Customizing the agent
|
||||
|
||||
The prebuilt agent lets us get started quickly, but at each step the agent has access to the full set of tools. Above, we relied on the system prompt to constrain its behavior— for example, we instructed the agent to always start with the "list tables" tool, and to always run a query-checker tool before executing the query.
|
||||
|
||||
We can enforce a higher degree of control in LangGraph by customizing the agent. Below, we implement a simple ReAct-agent setup, with dedicated nodes for specific tool-calls. We will use the same [state](../../concepts/low_level.md#state) as the pre-built agent.
|
||||
|
||||
We construct dedicated nodes for the following steps:
|
||||
|
||||
- Listing DB tables
|
||||
- Calling the "get schema" tool
|
||||
- Generating a query
|
||||
- Checking the query
|
||||
|
||||
Putting these steps in dedicated nodes lets us (1) force tool-calls when needed, and (2) customize the prompts associated with each step.
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import END, START, MessagesState, StateGraph
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
|
||||
get_schema_tool = next(tool for tool in tools if tool.name == "sql_db_schema")
|
||||
get_schema_node = ToolNode([get_schema_tool], name="get_schema")
|
||||
|
||||
run_query_tool = next(tool for tool in tools if tool.name == "sql_db_query")
|
||||
run_query_node = ToolNode([run_query_tool], name="run_query")
|
||||
|
||||
|
||||
# Example: create a predetermined tool call
|
||||
def list_tables(state: MessagesState):
|
||||
tool_call = {
|
||||
"name": "sql_db_list_tables",
|
||||
"args": {},
|
||||
"id": "abc123",
|
||||
"type": "tool_call",
|
||||
}
|
||||
tool_call_message = AIMessage(content="", tool_calls=[tool_call])
|
||||
|
||||
list_tables_tool = next(tool for tool in tools if tool.name == "sql_db_list_tables")
|
||||
tool_message = list_tables_tool.invoke(tool_call)
|
||||
response = AIMessage(f"Available tables: {tool_message.content}")
|
||||
|
||||
return {"messages": [tool_call_message, tool_message, response]}
|
||||
|
||||
|
||||
# Example: force a model to create a tool call
|
||||
def call_get_schema(state: MessagesState):
|
||||
# Note that LangChain enforces that all models accept `tool_choice="any"`
|
||||
# as well as `tool_choice=<string name of tool>`.
|
||||
llm_with_tools = llm.bind_tools([get_schema_tool], tool_choice="any")
|
||||
response = llm_with_tools.invoke(state["messages"])
|
||||
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
generate_query_system_prompt = """
|
||||
You are an agent designed to interact with a SQL database.
|
||||
Given an input question, create a syntactically correct {dialect} query to run,
|
||||
then look at the results of the query and return the answer. Unless the user
|
||||
specifies a specific number of examples they wish to obtain, always limit your
|
||||
query to at most {top_k} results.
|
||||
|
||||
You can order the results by a relevant column to return the most interesting
|
||||
examples in the database. Never query for all the columns from a specific table,
|
||||
only ask for the relevant columns given the question.
|
||||
|
||||
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.
|
||||
""".format(
|
||||
dialect=db.dialect,
|
||||
top_k=5,
|
||||
)
|
||||
|
||||
|
||||
def generate_query(state: MessagesState):
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": generate_query_system_prompt,
|
||||
}
|
||||
# We do not force a tool call here, to allow the model to
|
||||
# respond naturally when it obtains the solution.
|
||||
llm_with_tools = llm.bind_tools([run_query_tool])
|
||||
response = llm_with_tools.invoke([system_message] + state["messages"])
|
||||
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
check_query_system_prompt = """
|
||||
You are a SQL expert with a strong attention to detail.
|
||||
Double check the {dialect} query for common mistakes, including:
|
||||
- Using NOT IN with NULL values
|
||||
- Using UNION when UNION ALL should have been used
|
||||
- Using BETWEEN for exclusive ranges
|
||||
- Data type mismatch in predicates
|
||||
- Properly quoting identifiers
|
||||
- Using the correct number of arguments for functions
|
||||
- Casting to the correct data type
|
||||
- Using the proper columns for joins
|
||||
|
||||
If there are any of the above mistakes, rewrite the query. If there are no mistakes,
|
||||
just reproduce the original query.
|
||||
|
||||
You will call the appropriate tool to execute the query after running this check.
|
||||
""".format(dialect=db.dialect)
|
||||
|
||||
|
||||
def check_query(state: MessagesState):
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": check_query_system_prompt,
|
||||
}
|
||||
|
||||
# Generate an artificial user message to check
|
||||
tool_call = state["messages"][-1].tool_calls[0]
|
||||
user_message = {"role": "user", "content": tool_call["args"]["query"]}
|
||||
llm_with_tools = llm.bind_tools([run_query_tool], tool_choice="any")
|
||||
response = llm_with_tools.invoke([system_message, user_message])
|
||||
response.id = state["messages"][-1].id
|
||||
|
||||
return {"messages": [response]}
|
||||
```
|
||||
|
||||
Finally, we assemble these steps into a workflow using the Graph API. We define a [conditional edge](../../concepts/low_level.md#conditional-edges) at the query generation step that will route to the query checker if a query is generated, or end if there are no tool calls present, such that the LLM has delivered a response to the query.
|
||||
|
||||
```python
|
||||
def should_continue(state: MessagesState) -> Literal[END, "check_query"]:
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
if not last_message.tool_calls:
|
||||
return END
|
||||
else:
|
||||
return "check_query"
|
||||
|
||||
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(list_tables)
|
||||
builder.add_node(call_get_schema)
|
||||
builder.add_node(get_schema_node, "get_schema")
|
||||
builder.add_node(generate_query)
|
||||
builder.add_node(check_query)
|
||||
builder.add_node(run_query_node, "run_query")
|
||||
|
||||
builder.add_edge(START, "list_tables")
|
||||
builder.add_edge("list_tables", "call_get_schema")
|
||||
builder.add_edge("call_get_schema", "get_schema")
|
||||
builder.add_edge("get_schema", "generate_query")
|
||||
builder.add_conditional_edges(
|
||||
"generate_query",
|
||||
should_continue,
|
||||
)
|
||||
builder.add_edge("check_query", "run_query")
|
||||
builder.add_edge("run_query", "generate_query")
|
||||
|
||||
agent = builder.compile()
|
||||
```
|
||||
|
||||
We visualize the application below:
|
||||
|
||||
```python
|
||||
from IPython.display import Image, display
|
||||
from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles
|
||||
|
||||
display(Image(agent.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Note:** When you run this code, it will generate and display a visual representation of the SQL agent graph showing the flow between the different nodes (list_tables → call_get_schema → get_schema → generate_query → check_query → run_query).
|
||||
|
||||
We can now invoke the graph exactly as before:
|
||||
|
||||
```python
|
||||
question = "Which genre on average has the longest tracks?"
|
||||
|
||||
for step in agent.stream(
|
||||
{"messages": [{"role": "user", "content": question}]},
|
||||
stream_mode="values",
|
||||
):
|
||||
step["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
================================ Human Message =================================
|
||||
|
||||
Which genre on average has the longest tracks?
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Available tables: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_schema (call_qxKtYiHgf93AiTDin9ez5wFp)
|
||||
Call ID: call_qxKtYiHgf93AiTDin9ez5wFp
|
||||
Args:
|
||||
table_names: Genre,Track
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_schema
|
||||
|
||||
|
||||
CREATE TABLE "Genre" (
|
||||
"GenreId" INTEGER NOT NULL,
|
||||
"Name" NVARCHAR(120),
|
||||
PRIMARY KEY ("GenreId")
|
||||
)
|
||||
|
||||
/*
|
||||
3 rows from Genre table:
|
||||
GenreId Name
|
||||
1 Rock
|
||||
2 Jazz
|
||||
3 Metal
|
||||
*/
|
||||
|
||||
|
||||
CREATE TABLE "Track" (
|
||||
"TrackId" INTEGER NOT NULL,
|
||||
"Name" NVARCHAR(200) NOT NULL,
|
||||
"AlbumId" INTEGER,
|
||||
"MediaTypeId" INTEGER NOT NULL,
|
||||
"GenreId" INTEGER,
|
||||
"Composer" NVARCHAR(220),
|
||||
"Milliseconds" INTEGER NOT NULL,
|
||||
"Bytes" INTEGER,
|
||||
"UnitPrice" NUMERIC(10, 2) NOT NULL,
|
||||
PRIMARY KEY ("TrackId"),
|
||||
FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
|
||||
FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
|
||||
FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
|
||||
)
|
||||
|
||||
/*
|
||||
3 rows from Track table:
|
||||
TrackId Name AlbumId MediaTypeId GenreId Composer Milliseconds Bytes UnitPrice
|
||||
1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99
|
||||
2 Balls to the Wall 2 2 1 None 342562 5510424 0.99
|
||||
3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99
|
||||
*/
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_query (call_RPN3GABMfb6DTaFTLlwnZxVN)
|
||||
Call ID: call_RPN3GABMfb6DTaFTLlwnZxVN
|
||||
Args:
|
||||
query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgTrackLength
|
||||
FROM Track
|
||||
JOIN Genre ON Track.GenreId = Genre.GenreId
|
||||
GROUP BY Genre.GenreId
|
||||
ORDER BY AvgTrackLength DESC
|
||||
LIMIT 1;
|
||||
================================== Ai Message ==================================
|
||||
Tool Calls:
|
||||
sql_db_query (call_PR4s8ymiF3ZQLaoZADXtdqcl)
|
||||
Call ID: call_PR4s8ymiF3ZQLaoZADXtdqcl
|
||||
Args:
|
||||
query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgTrackLength
|
||||
FROM Track
|
||||
JOIN Genre ON Track.GenreId = Genre.GenreId
|
||||
GROUP BY Genre.GenreId
|
||||
ORDER BY AvgTrackLength DESC
|
||||
LIMIT 1;
|
||||
================================= Tool Message =================================
|
||||
Name: sql_db_query
|
||||
|
||||
[('Sci Fi & Fantasy', 2911783.0384615385)]
|
||||
================================== Ai Message ==================================
|
||||
|
||||
The genre with the longest tracks on average is "Sci Fi & Fantasy," with an average track length of approximately 2,911,783 milliseconds.
|
||||
```
|
||||
|
||||
!!! tip
|
||||
See [LangSmith trace](https://smith.langchain.com/public/94b8c9ac-12f7-4692-8706-836a1f30f1ea/r) for the above run.
|
||||
|
||||
## Next steps
|
||||
|
||||
Check out [this guide](https://docs.smith.langchain.com/evaluation/how_to_guides/langgraph) for evaluating LangGraph applications, including SQL agents like this one, using LangSmith.
|
||||
@@ -109,6 +109,7 @@ nav:
|
||||
- Agent architectures: concepts/agentic_concepts.md
|
||||
|
||||
- Guides:
|
||||
- guides/index.md
|
||||
- LangGraph APIs:
|
||||
- Graph API:
|
||||
- Overview: concepts/low_level.md
|
||||
@@ -268,25 +269,26 @@ nav:
|
||||
- Environment variables: cloud/reference/env_var.md
|
||||
|
||||
- Examples:
|
||||
- examples/index.md
|
||||
- Template applications: concepts/template_applications.md # TODO: make tutorial
|
||||
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.ipynb
|
||||
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- SQL agent: tutorials/sql-agent.ipynb
|
||||
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.md
|
||||
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.md
|
||||
- SQL agent: tutorials/sql/sql-agent.md
|
||||
- Prebuilt chat UI: agents/ui.md
|
||||
- Graph runs in LangSmith: how-tos/run-id-langsmith.ipynb
|
||||
- Graph runs in LangSmith: how-tos/run-id-langsmith.md
|
||||
- LangGraph Platform:
|
||||
- Authentication:
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- Use RemoteGraph: how-tos/use-remote-graph.md
|
||||
- Deploy CrewAI, AutoGen, and other frameworks: how-tos/autogen-langgraph-platform.ipynb
|
||||
# combine with how-tos/autogen-integration.ipynb
|
||||
- Deploy CrewAI, AutoGen, and other frameworks: how-tos/autogen-integration.md
|
||||
- Front-end and generative UI:
|
||||
- Integrate LangGraph into a React app: cloud/how-tos/use_stream_react.md
|
||||
- Implement generative UI with LangGraph: cloud/how-tos/generative_ui_react.md
|
||||
|
||||
- Additional resources:
|
||||
- additional-resources/index.md
|
||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- LangGraph Academy course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||
- Case studies: adopters.md
|
||||
|
||||