diff --git a/README.md b/README.md index ad8725a89..e2a4e30e9 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,6 @@ functions = [format_tool_to_openai_function(t) for t in tools] model = model.bind_functions(functions) ``` - ### Define the agent state The main type of graph in `langgraph` is the `StatefulGraph`. @@ -293,7 +292,7 @@ Output from node '__end__': ### Streaming LLM Tokens -You can also access the LLM tokens as they are produced by each node. +You can also access the LLM tokens as they are produced by each node. In this case only the "agent" node produces LLM tokens. In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True)`) @@ -417,10 +416,9 @@ Langchain Expression Language allows you to easily define chains (DAGs) but does ## Examples - ### ChatAgentExecutor: with function calling -This agent executor takes a list of messages as input and outputs a list of messages. +This agent executor takes a list of messages as input and outputs a list of messages. All agent state is represented as a list of messages. This specifically uses OpenAI function calling. This is recommended agent executor for newer chat based models that support function calling. @@ -431,6 +429,7 @@ This is recommended agent executor for newer chat based models that support func **Modifications** We also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the [getting started notebook](examples/chat_agent_executor_with_function_calling/base.ipynb) so it is recommended you start with that first. + - [Human-in-the-loop](https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb): How to add a human-in-the-loop component - [Force calling a tool first](https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb): How to always call a specific tool first - [Respond in a specific format](https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb): How to force the agent to respond in a specific format @@ -447,10 +446,23 @@ This agent executor uses existing LangChain agents. **Modifications** We also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the [getting started notebook](examples/agent_executor/base.ipynb) so it is recommended you start with that first. + - [Human-in-the-loop](https://github.com/langchain-ai/langgraph/blob/main/examples/agent_executor/human-in-the-loop.ipynb): How to add a human-in-the-loop component - [Force calling a tool first](https://github.com/langchain-ai/langgraph/blob/main/examples/agent_executor/force-calling-a-tool-first.ipynb): How to always call a specific tool first - [Managing agent steps](https://github.com/langchain-ai/langgraph/blob/main/examples/agent_executor/managing-agent-steps.ipynb): How to more explicitly manage intermediate steps that an agent takes +### Multi-agent Examples + +- [Multi-agent collaboration](https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/multi-agent-collaboration.ipynb): how to create two agents that work together to accomplish a task +- [Multi-agent with supervisor](https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/agent_supervisor.ipynb): how to orchestrate individual agents by using an LLM as a "supervisor" to distribute work +- [Hierarchical agent teams](https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/hierarchical_agent_teams.ipynb): how to orchestrate "teams" of agents as nested graphs that can collaborate to solve a problem + +### Chatbot Evaluation via Simulation + +It can often be tough to evaluation chat bots in multi-turn situations. One way to do this is with simulations. + +- [Chat bot evaluation as multi-agent simulation](https://github.com/langchain-ai/langgraph/blob/main/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb): How to simulate a dialogue between a "virtual user" and your chat bot + ### Async If you are running LangGraph in async workflows, you may want to create the nodes to be async by default. @@ -477,7 +489,6 @@ This class is responsible for constructing the graph. It exposes an interface inspired by [NetworkX](https://networkx.org/documentation/latest/). This graph is parameterized by a state object that it passes around to each node. - #### `__init__` ```python @@ -633,7 +644,6 @@ It can be used in two places: - As the `end_key` in `add_edge` - As a value in `conditional_edge_mapping` as passed to `add_conditional_edges` - ## Prebuilt Examples There are also a few methods we've added to make it easy to use common, prebuilt graphs and components. diff --git a/examples/advanced_agents/multi-agent/img/virtual_user_annotate.png b/examples/advanced_agents/multi-agent/img/virtual_user_annotate.png deleted file mode 100644 index 8886efae4..000000000 Binary files a/examples/advanced_agents/multi-agent/img/virtual_user_annotate.png and /dev/null differ diff --git a/examples/advanced_agents/multi-agent/img/virtual_user_full_convo.png b/examples/advanced_agents/multi-agent/img/virtual_user_full_convo.png deleted file mode 100644 index 8da9c2d3c..000000000 Binary files a/examples/advanced_agents/multi-agent/img/virtual_user_full_convo.png and /dev/null differ diff --git a/examples/advanced_agents/multi-agent/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb similarity index 98% rename from examples/advanced_agents/multi-agent/agent-simulation-evaluation.ipynb rename to examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index 13ebb315a..0510118be 100644 --- a/examples/advanced_agents/multi-agent/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -196,9 +196,8 @@ "\n", "The code below creates a LangGraph workflow to run the simulation. The main components are:\n", "\n", - "1. Simulation state: the inputs to each node in the graph, containing the messages.\n", - "2. The two nodes: one for the simulated user, the other for the chat bot.\n", - "3. The graph itself, with a conditional stopping criterion.\n", + "1. The two nodes: one for the simulated user, the other for the chat bot.\n", + "2. The graph itself, with a conditional stopping criterion.\n", "\n", "Read the comments in the code below for more information.\n" ] diff --git a/examples/advanced_agents/multi-agent/img/virtual_user_diagram.png b/examples/chatbot-simulation-evaluation/img/virtual_user_diagram.png similarity index 100% rename from examples/advanced_agents/multi-agent/img/virtual_user_diagram.png rename to examples/chatbot-simulation-evaluation/img/virtual_user_diagram.png diff --git a/examples/multi_agent/agent_supervisor.ipynb b/examples/multi_agent/agent_supervisor.ipynb new file mode 100644 index 000000000..4895d1486 --- /dev/null +++ b/examples/multi_agent/agent_supervisor.ipynb @@ -0,0 +1,419 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a3e3ebc4-57af-4fe4-bdd3-36aff67bf276", + "metadata": {}, + "source": [ + "## Agent Supervisor\n", + "\n", + "The [previous example](multi-agent-collaboration.ipynb) routed messages automatically based on the output of the initial researcher agent.\n", + "\n", + "We can also choose to use an LLM to orchestrate the different agents.\n", + "\n", + "Below, we will create an agent group, with an agent supervisor to help delegate tasks.\n", + "\n", + "![diagram](./img/supervisor-diagram.png)\n", + "\n", + "To simplify the code in each agent node, we will use the AgentExecutor class from LangChain. This and other \"advanced agent\" notebooks are designed to show how you can implement certain design patterns in LangGraph. If the pattern suits your needs, we recommend combining it with some of the other fundamental patterns described elsewhere in the docs for best performance.\n", + "\n", + "Before we build, let's configure our environment:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", + "metadata": {}, + "outputs": [], + "source": [ + "# %%capture --no-stderr\n", + "# %pip install -U langchain langchain_openai langchain_experimental langsmith pandas" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] + }, + { + "cell_type": "markdown", + "id": "1ac25624-4d83-45a4-b9ef-a10589aacfb7", + "metadata": {}, + "source": [ + "## Create tools\n", + "\n", + "For this example, you will make an agent to do web research with a search engine, and one agent to create plots. Define the tools they'll use below:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f04c6778-403b-4b49-9b93-678e910d5cec", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated, List, Tuple, Union\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", + "from langchain_experimental.tools import PythonREPLTool\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "# This executes code locally, which can be unsafe\n", + "python_repl_tool = PythonREPLTool()" + ] + }, + { + "cell_type": "markdown", + "id": "d58d1e85-22d4-4c22-9062-72a346a0d709", + "metadata": {}, + "source": [ + "## Helper Utilites\n", + "\n", + "Define a helper function below, which make it easier to add new agent worker nodes." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c4823dd9-26bd-4e1a-8117-b97b2860211a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import AgentExecutor, create_openai_tools_agent\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "\n", + "def create_worker_node(\n", + " workflow: StateGraph, name: str, llm: ChatOpenAI, tools: list, system_prompt: str\n", + "):\n", + " # Each worker node will be given a name and some tools.\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " system_prompt,\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", + " ]\n", + " )\n", + " agent = create_openai_tools_agent(llm, tools, prompt)\n", + " executor = AgentExecutor(agent=agent, tools=tools)\n", + " chain = executor | (\n", + " # So the agents properly role-play in this simulation, we will\n", + " # tag their final message as a human message\n", + " lambda x: {\"messages\": [HumanMessage(content=x[\"output\"], name=name)]}\n", + " )\n", + " workflow.add_node(name, chain)" + ] + }, + { + "cell_type": "markdown", + "id": "a07d507f-34d1-4f1b-8dde-5e58d17b2166", + "metadata": {}, + "source": [ + "## Construct Graph\n", + "\n", + "We're ready to start building the graph. Below, define the state and worker nodes using the function we just defined." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8", + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, Any, Dict, List, Optional, Sequence, TypedDict\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "\n", + "\n", + "# The agent state is the input to each node in the graph\n", + "class AgentState(TypedDict):\n", + " # The annotation tells the graph that new messages will always\n", + " # be added to the current states\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + " # The 'next' field indicates where to route to next\n", + " next: str\n", + "\n", + "\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", + "\n", + "\n", + "create_worker_node(\n", + " workflow, \"Researcher\", llm, [tavily_tool], \"You are a web researcher.\"\n", + ")\n", + "# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n", + "create_worker_node(\n", + " workflow,\n", + " \"Coder\",\n", + " llm,\n", + " [python_repl_tool],\n", + " \"You may generate safe python code to analyze data \"\n", + " \"and generate charts using matplotlib.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d6374825-912f-40c9-910d-afa267b401bf", + "metadata": {}, + "source": [ + "Almost done, now create create the team supervisor. It will use function calling to choose the next worker node OR finish processing." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "17c108a0-6dc3-46fd-a5e6-a1fcfad5458a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n", + "\n", + "members = [\"Researcher\", \"Coder\"]\n", + "system_prompt = (\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\"\n", + ")\n", + "# Our team supervisor is an LLM node. It just picks the next agent to process\n", + "# and decides when the work is completed\n", + "options = [\"FINISH\"] + members\n", + "# Using openai function calling can make output parsing easier for us\n", + "function_def = {\n", + " \"name\": \"route\",\n", + " \"description\": \"Select the next role.\",\n", + " \"parameters\": {\n", + " \"title\": \"routeSchema\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"next\": {\n", + " \"title\": \"Next\",\n", + " \"anyOf\": [\n", + " {\"enum\": options},\n", + " ],\n", + " }\n", + " },\n", + " \"required\": [\"next\"],\n", + " },\n", + "}\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + ").partial(options=str(options), members=\", \".join(members))\n", + "\n", + "supervisor_chain = (\n", + " prompt\n", + " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", + " | JsonOutputFunctionsParser()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2c1593d5-39f7-4819-96d2-4ad7d7991d72", + "metadata": {}, + "source": [ + "Now connect all the edges in the graph." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "14778e86-077b-4e6a-893c-400e59b0cdbf", + "metadata": {}, + "outputs": [], + "source": [ + "workflow.add_node(\"supervisor\", supervisor_chain)\n", + "\n", + "\n", + "for member in members:\n", + " # We want our workers to ALWAYS \"report back\" to the supervisor when done\n", + " workflow.add_edge(member, \"supervisor\")\n", + "# The supervisor populates the \"next\" field in the graph state\n", + "# which routes to a node or finishes\n", + "conditional_map = {k: k for k in members}\n", + "conditional_map[\"FINISH\"] = END\n", + "workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n", + "# Finally, add entrypoint\n", + "workflow.set_entry_point(\"supervisor\")\n", + "\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "d36496de-7121-4c49-8cb6-58c943c66628", + "metadata": {}, + "source": [ + "## Invoke the team\n", + "\n", + "With the graph created, we can now invoke it and see how it performs!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "56ba78e9-d9c1-457c-a073-d606d5d3e013", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Python REPL can execute arbitrary code. Use with caution.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "The code `print('Hello, World!')` was executed, and the output is:\n", + "\n", + "```\n", + "Hello, World!\n", + "```\n" + ] + } + ], + "source": [ + "results = graph.invoke(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(content=\"Code hello world and print it to the terminal\")\n", + " ]\n", + " }\n", + ")\n", + "results[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "45a92dfd-0e11-47f5-aad4-b68d24990e34", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "# Research Report on Pikas\n", + "\n", + "Pikas are small, mountain-dwelling mammals that are closely related to rabbits. They are known for their distinctive chirps and typically inhabit boulder fields at high elevations, up to 14,000 feet in treeless slopes like those found in the Southern Rockies. These animals are recognized for their ability to adapt to some of the most inhospitable climates.\n", + "\n", + "## Climate Change Impact\n", + "\n", + "Pikas have been a topic of interest in climate change research due to their sensitivity to high temperatures and reliance on cold habitats. They have historically responded to climate shifts by moving to higher elevations or latitudes to find suitable cooler environments. For instance, pikas were once found in the Appalachian Mountains and even in the Mojave Desert, but as the Earth's climate warmed, they moved to cooler, high-elevation areas where they live today.\n", + "\n", + "Recent studies suggest that pikas are showing remarkable adaptability to climate change. Despite predictions that they might become endangered due to rising temperatures, these animals are displaying resilience. Some research indicates that pikas can adjust certain genes to make better use of oxygen in higher altitudes where the air is thinner, which could be a potential hope for their survival as climate change drives them to higher elevations.\n", + "\n", + "However, there have been reports of pikas disappearing from parts of the Great Basin, and in Colorado, pikas have retracted upslope by about 1,160 feet. It's been noted that while climate change may be a factor, it might not be the sole cause for these local disappearances.\n", + "\n", + "## Adaptation Strategies\n", + "\n", + "Pikas exhibit several interesting behaviors that help them cope with their challenging environment. During the summer, they engage in activities like \"making hay\" — collecting and storing vegetation in preparation for the harsh winters. Their diet and caching behavior are essential for their survival during the months when food is scarce.\n", + "\n", + "## Conservation and Research\n", + "\n", + "Conservationists and scientists continue to study pikas to understand their adaptation mechanisms and how they might inform broader climate change mitigation strategies. For example, studies have been conducted on pikas at different elevations to observe genetic changes and their effects on adaptation. Such research is crucial for predicting the future of pikas and potentially other species affected by climate change.\n", + "\n", + "## Conclusion\n", + "\n", + "Pikas serve as an important indicator species for the impacts of climate change on wildlife. Their ability to adapt to changing climates offers hope and also highlights the importance of understanding genetic adaptability in the face of environmental challenges. Conservation efforts and further research are essential to ensure the survival of pikas and to learn from their resilience.\n", + "\n", + "### Sources\n", + "- [The Conversation: Pikas are adapting to climate change remarkably well](https://theconversation.com/pikas-are-adapting-to-climate-change-remarkably-well-contrary-to-many-predictions-150726)\n", + "- [Stanford Sustainability: It's in the genes – potential hope for pikas hit by climate change](https://sustainability.stanford.edu/news/its-genes-potential-hope-pikas-hit-climate-change)\n", + "- [Colorado Sun: Colorado pika population and climate change](https://coloradosun.com/2023/08/27/colorado-pika-population-climate-change/)\n", + "- [PetaPixel: Photographing the American Pika – a tiny indicator of climate change](https://petapixel.com/2022/01/03/photographing-the-american-pika-a-tiny-indicator-of-climate-change/)\n", + "- [The Wildlife Society: Can pikas survive climate change after all?](https://wildlife.org/can-pikas-survive-climate-change-after-all/)\n" + ] + } + ], + "source": [ + "results = graph.invoke(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(content=\"Write a brief research report on pikas.\")\n", + " ]\n", + " },\n", + " {\"recursion_limit\": 100},\n", + ")\n", + "results[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_agent/hierarchical_agent_teams.ipynb b/examples/multi_agent/hierarchical_agent_teams.ipynb new file mode 100644 index 000000000..801ec2e32 --- /dev/null +++ b/examples/multi_agent/hierarchical_agent_teams.ipynb @@ -0,0 +1,642 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a3e3ebc4-57af-4fe4-bdd3-36aff67bf276", + "metadata": {}, + "source": [ + "## Hierarchical Agent Teams\n", + "\n", + "In our previous example ([Agent Supervisor](./agent_supervisor.ipynb)), we introduced the concept of a single supervisor node to route work between different worker nodes.\n", + "\n", + "But what if the job for a single worker becomes too complex? What if the number of workers becomes too large?\n", + "\n", + "For some applications, the system may be more effective if work is distributed _hierarchically_.\n", + "\n", + "You can do this by composing different subgraphs and creating a top-level supervisor, along with mid-level supervisors.\n", + "\n", + "To do this, let's build a simple research assistant! The graph will look something like the following:\n", + "\n", + "![diagram](./img/hierarchical-diagram.png)\n", + "\n", + "This notebook is inspired by the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://arxiv.org/abs/2308.08155), by Wu, et. al. In the rest of this notebook, you will:\n", + "\n", + "1. Define some utilities to help create the graph and their relations\n", + "2. Write the tools and agent implementations for each team\n", + "3. Compose everything together.\n", + "\n", + "But before all of that, some setup:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", + "metadata": {}, + "outputs": [], + "source": [ + "# %%capture --no-stderr\n", + "# %pip install -U langgraph langchain langchain_openai langchain_experimental" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "import uuid\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith.\n", + "# This will help you visualize and debug the control flow\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] + }, + { + "cell_type": "markdown", + "id": "504ee1c6-2b6a-439d-9046-df54e1e15698", + "metadata": {}, + "source": [ + "## Define Utilities\n", + "\n", + "We are going to create a few utility functions to make it more concise when we want to:\n", + "\n", + "1. Create a worker agent and add it to a graph.\n", + "2. Create a supervisor for the sub-graph.\n", + "\n", + "These will simplify the graph compositional code at the end for us so it's easier to see what's going on." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e09fb60f-1aac-455b-b67d-8d2e4ccfd747", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any, Callable, List, Optional, TypedDict, Union\n", + "\n", + "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables import Runnable\n", + "from langchain_core.tools import BaseTool\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "\n", + "def create_worker_agent(\n", + " graph_builder: StateGraph,\n", + " name: str,\n", + " llm: ChatOpenAI,\n", + " tools: list,\n", + " system_prompt: str,\n", + " prelude: Optional[Union[Runnable, Callable]] = None, # Optional required steps\n", + ") -> str:\n", + " \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n", + " system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n", + " \" Do not ask for clarification.\"\n", + " \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n", + " \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " system_prompt,\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", + " ]\n", + " )\n", + " agent = create_openai_functions_agent(llm, tools, prompt)\n", + " executor = AgentExecutor(agent=agent, tools=tools)\n", + " chain = executor | (\n", + " lambda x: {\"messages\": [HumanMessage(content=x[\"output\"], name=name)]}\n", + " )\n", + " if prelude is not None:\n", + " chain = prelude | chain\n", + " graph_builder.add_node(name, chain)\n", + " return name\n", + "\n", + "\n", + "def create_team_supervisor(\n", + " graph_builder: StateGraph, llm: ChatOpenAI, system_prompt: str\n", + ") -> str:\n", + " \"\"\"An LLM-based router.\"\"\"\n", + " supervisor_id = uuid.uuid4().hex[:4]\n", + " supervisor_name = f\"supervisor - {supervisor_id}\"\n", + " members = list(graph_builder.nodes)\n", + " options = [\"FINISH\"] + members\n", + " function_def = {\n", + " \"name\": \"route\",\n", + " \"description\": \"Select the next role.\",\n", + " \"parameters\": {\n", + " \"title\": \"routeSchema\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"next\": {\n", + " \"title\": \"Next\",\n", + " \"anyOf\": [\n", + " {\"enum\": options},\n", + " ],\n", + " },\n", + " },\n", + " \"required\": [\"next\"],\n", + " },\n", + " }\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + " ).partial(options=str(options), team_members=\", \".join(members))\n", + " chain = (\n", + " prompt\n", + " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", + " | JsonOutputFunctionsParser()\n", + " )\n", + " graph_builder.add_node(supervisor_name, chain)\n", + " conditional_map = {k: k for k in members}\n", + " conditional_map[\"FINISH\"] = END\n", + "\n", + " for member in members:\n", + " graph_builder.add_edge(member, supervisor_name)\n", + " graph_builder.add_conditional_edges(\n", + " supervisor_name, lambda x: x[\"next\"], conditional_map\n", + " )\n", + " return supervisor_name" + ] + }, + { + "cell_type": "markdown", + "id": "00282b1f-bb4d-4ee7-9bae-e8e6f586f12e", + "metadata": {}, + "source": [ + "## Define agents + tools\n", + "\n", + "Now we can get to define our hierachical teams. \"Choose your player!\"\n", + "\n", + "### Research Team\n", + "\n", + "The research team can use a search engine and url scraper to find information on the web. Feel free to add additional functionality below to boost the team performance!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f04c6778-403b-4b49-9b93-678e910d5cec", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated, List, Tuple, Union\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", + "from langsmith import trace\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "\n", + "@tool\n", + "def scrape_webpages(urls: List[str]) -> str:\n", + " \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n", + " loader = WebBaseLoader(urls)\n", + " docs = loader.load()\n", + " return \"\\n\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "53db0c78-e357-48ba-ae5f-3fc04735a3b7", + "metadata": {}, + "outputs": [], + "source": [ + "import functools\n", + "import operator\n", + "\n", + "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "\n", + "\n", + "# Research team graph state\n", + "class State(TypedDict):\n", + " # A message is added after each team member finishes\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # The team members are tracked so they are aware of\n", + " # the others' skill-sets\n", + " team_members: List[str]\n", + " # Used to route work. The supervisor calls a function\n", + " # that will update this every time it makes a decision\n", + " next: str\n", + "\n", + "\n", + "research_graph = StateGraph(State)\n", + "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", + "create_worker_agent(\n", + " research_graph,\n", + " \"Search\",\n", + " llm,\n", + " [tavily_tool],\n", + " \"You are a research assistant who can search for up-to-date info\"\n", + " \" using the tavily search engine.\",\n", + ")\n", + "create_worker_agent(\n", + " research_graph,\n", + " \"Web Scraper\",\n", + " llm,\n", + " [scrape_webpages],\n", + " \"You are a research assistant who can scrape\"\n", + " \" specified urls for more detailed information using\"\n", + " \" the scrape_webpages function.\",\n", + ")\n", + "supervisor_node = create_team_supervisor(\n", + " research_graph,\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + ")\n", + "\n", + "research_graph.set_entry_point(supervisor_node)\n", + "\n", + "\n", + "# The following functions interoperate between the top level graph state\n", + "# and the state of the research sub-graph\n", + "# this makes it so that the states of each graph don't get intermixed\n", + "def enter_chain(message: str, members: Optional[list] = None):\n", + " results = {\n", + " \"messages\": [HumanMessage(content=message)],\n", + " }\n", + " if members:\n", + " results[\"team_members\"] = \"\\n\".join(sorted(members))\n", + " return results\n", + "\n", + "\n", + "def return_final_response(state):\n", + " return {\"final_response\": state[\"messages\"][-1]}\n", + "\n", + "\n", + "research_chain = (\n", + " functools.partial(enter_chain, members=research_graph.nodes)\n", + " | research_graph.compile()\n", + " | return_final_response\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "749b99ab-f6f0-4c5d-a90b-10102465d186", + "metadata": {}, + "source": [ + "## Document Writing Team\n", + "\n", + "We will construct a graph in a similar fashion. This time using different tools.\n", + "\n", + "Note that we are giving file-system access to our agent here, which is not safe in all cases." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "202806d6-80bf-4153-ac16-ed6059236f2a", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from tempfile import TemporaryDirectory\n", + "from typing import Dict\n", + "\n", + "from langchain_experimental.utilities import PythonREPL\n", + "from typing_extensions import TypedDict\n", + "\n", + "_TEMP_DIRECTORY = TemporaryDirectory()\n", + "WORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n", + "\n", + "\n", + "@tool\n", + "def create_outline(\n", + " points: Annotated[List[str], \"List of main points or sections.\"],\n", + " file_name: Annotated[str, \"File path to save the outline.\"],\n", + ") -> Annotated[str, \"Path of the saved outline file.\"]:\n", + " \"\"\"Create and save an outline.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " for i, point in enumerate(points):\n", + " file.write(f\"{i + 1}. {point}\\n\")\n", + " return f\"Outline saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def read_document(\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + " start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n", + " end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n", + ") -> str:\n", + " \"\"\"Read the specified document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + " if start is not None:\n", + " start = 0\n", + " return \"\\n\".join(lines[start:end])\n", + "\n", + "\n", + "@tool\n", + "def write_document(\n", + " content: Annotated[str, \"Text content to be written into the document.\"],\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + ") -> Annotated[str, \"Path of the saved document file.\"]:\n", + " \"\"\"Create and save a text document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.write(content)\n", + " return f\"Document saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def edit_document(\n", + " file_name: Annotated[str, \"Path of the document to be edited.\"],\n", + " inserts: Annotated[\n", + " Dict[int, str],\n", + " \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n", + " ],\n", + ") -> Annotated[str, \"Path of the edited document file.\"]:\n", + " \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n", + " # Read the contents of the file\n", + "\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + "\n", + " # Adjust the line numbers for 0-indexing and sort\n", + " sorted_inserts = sorted(inserts.items())\n", + "\n", + " # Perform the insertions\n", + " for line_number, text in sorted_inserts:\n", + " if 1 <= line_number <= len(lines) + 1:\n", + " # Insert the text at the specified line number\n", + " lines.insert(line_number - 1, text + \"\\n\")\n", + " else:\n", + " return f\"Error: Line number {line_number} is out of range.\"\n", + "\n", + " # Write the modified content back to the file\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.writelines(lines)\n", + "\n", + " return f\"Document edited and saved to {file_name}\"\n", + "\n", + "\n", + "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", + "\n", + "repl = PythonREPL()\n", + "\n", + "\n", + "@tool\n", + "def python_repl(\n", + " code: Annotated[str, \"The python code to execute to generate your chart.\"]\n", + "):\n", + " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", + " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", + " try:\n", + " result = repl.run(code)\n", + " except BaseException as e:\n", + " return f\"Failed to execute. Error: {repr(e)}\"\n", + " return f\"Succesfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1bcdbf44-9481-430c-8429-fa142ed8a626", + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from pathlib import Path\n", + "\n", + "\n", + "# Document writing team graph state\n", + "class AuthoringState(TypedDict):\n", + " # This tracks the team's conversation internally\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # This provides each worker with context on the others' skill sets\n", + " team_members: str\n", + " # This is how the supervisor tells langgraph who to work next\n", + " next: str\n", + " # This tracks the shared directory state\n", + " current_files: str\n", + "\n", + "\n", + "# This will be run before each worker agent begins work\n", + "# It makes it so they are more aware of the current state\n", + "# of the working directory.\n", + "def prelude(state):\n", + " written_files = []\n", + " if not WORKING_DIRECTORY.exists():\n", + " WORKING_DIRECTORY.mkdir()\n", + " try:\n", + " written_files = [\n", + " f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n", + " ]\n", + " except:\n", + " pass\n", + " if not written_files:\n", + " return {**state, \"current_files\": \"No files written.\"}\n", + " return {\n", + " **state,\n", + " \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n", + " + \"\\n\".join([f\" - {f}\" for f in written_files]),\n", + " }\n", + "\n", + "\n", + "# Create the graph here:\n", + "authoring_graph = StateGraph(AuthoringState)\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", + "create_worker_agent(\n", + " authoring_graph,\n", + " \"Author Docs\",\n", + " llm,\n", + " [write_document, edit_document, read_document],\n", + " \"You are an expert writing a research document.\\n\"\n", + " # The {current_files} value is populated automatically by the graph state\n", + " \"Below are files currently in your directory:\\n{current_files}\",\n", + " prelude=prelude,\n", + ")\n", + "create_worker_agent(\n", + " authoring_graph,\n", + " \"Outline + Notetaker\",\n", + " llm,\n", + " [create_outline, read_document],\n", + " \"You are an expert senior researcher tasked with writing a paper outline and\"\n", + " \" taking notes to craft a perfect paper.{current_files}\",\n", + " prelude=prelude,\n", + ")\n", + "create_worker_agent(\n", + " authoring_graph,\n", + " \"Generate Charts\",\n", + " llm,\n", + " [read_document, python_repl],\n", + " \"You are a data viz expert tasked with generating charts for a research project.\"\n", + " \"{current_files}\",\n", + ")\n", + "\n", + "supervisor_node = create_team_supervisor(\n", + " authoring_graph,\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + ")\n", + "\n", + "authoring_graph.set_entry_point(supervisor_node)\n", + "\n", + "# We re-use the enter/exit functions to wrap the graph\n", + "authoring_chain = (\n", + " functools.partial(enter_chain, members=authoring_graph.nodes)\n", + " | authoring_graph.compile()\n", + " | return_final_response\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f4b5b08d-9a9a-474a-94b4-f7aaa8ff19e6", + "metadata": {}, + "source": [ + "## Add Layers\n", + "\n", + "In this design, we are enforcing a top-down planning policy. We've created two graphs already, but we have to decide how to route work between the two.\n", + "\n", + "We'll create a _third_ graph to orchestrate the previous two, and add some connectors to define how this top-level state is shared between the different graphs." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "95ae7e52-92ed-41a3-88c4-21b6d7c8b041", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "\n", + "\n", + "# Research team graph\n", + "class State(TypedDict):\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " next: str\n", + "\n", + "\n", + "def get_last_message(state: State) -> str:\n", + " return state[\"messages\"][-1].content\n", + "\n", + "\n", + "def join_graph(response: dict):\n", + " return {\"messages\": [response[\"final_response\"]]}\n", + "\n", + "\n", + "super_graph = StateGraph(State)\n", + "super_graph.add_node(\"Research team\", get_last_message | research_chain | join_graph)\n", + "super_graph.add_node(\n", + " \"Paper writing team\", get_last_message | authoring_chain | join_graph\n", + ")\n", + "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", + "supervisor_node = create_team_supervisor(\n", + " super_graph,\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following teams: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + ")\n", + "\n", + "super_graph.set_entry_point(supervisor_node)\n", + "super_graph = enter_chain | super_graph.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6b8badbf-d728-44bd-a2a7-5b4e587c92fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "HumanMessage(content='The document titled \"North American Sturgeons: Overview\" has been successfully created and saved as `North_American_Sturgeons_Overview.txt`. This document includes key information on the distribution, habitat, and conservation status of various sturgeon species found in North America, as well as a simplified table summarizing these details.\\n\\nFor further reference or detailed information, you can access and read the document. If you require any additional information or updates to the document, please let me know.', name='Author Docs')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = super_graph.invoke(\n", + " \"Write a brief research report on the North American sturgeon. Include a chart.\",\n", + " {\"recursion_limit\": 150},\n", + ")\n", + "results[\"messages\"][-1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6ffcc7f-7b78-4ca5-8e0a-7c0ac08300fc", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_agent/img/hierarchical-diagram.png b/examples/multi_agent/img/hierarchical-diagram.png new file mode 100644 index 000000000..b3fa81275 Binary files /dev/null and b/examples/multi_agent/img/hierarchical-diagram.png differ diff --git a/examples/multi_agent/img/supervisor-diagram.png b/examples/multi_agent/img/supervisor-diagram.png new file mode 100644 index 000000000..84497aa4c Binary files /dev/null and b/examples/multi_agent/img/supervisor-diagram.png differ diff --git a/examples/multi_agent/multi-agent-collaboration.ipynb b/examples/multi_agent/multi-agent-collaboration.ipynb new file mode 100644 index 000000000..3974788d2 --- /dev/null +++ b/examples/multi_agent/multi-agent-collaboration.ipynb @@ -0,0 +1,397 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "39fd1948-b5c3-48c4-b10e-2ae7e8c83334", + "metadata": {}, + "source": [ + "# Basic Multi-agent Collaboration\n", + "\n", + "A single agent can usually operate effectively using a handful of tools within a single domain, but even using powerful models like `gpt-4`, it can be less effective at using many tools. \n", + "\n", + "One way to approach complicated tasks is through a \"divide-and-conquer\" approach: create an specialized agent for each task or domain and route tasks to the correct \"expert\".\n", + "\n", + "This notebook (inspired by the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://arxiv.org/abs/2308.08155), by Wu, et. al.) shows one way to do this using LangGraph.\n", + "\n", + "The resulting graph will look something like the following diagram:\n", + "\n", + "![multi_agent diagram](./img/simple_multi_agent_diagram.png)\n", + "\n", + "Before we get started, a quick note: this and other multi-agent notebooks are designed to show _how_ you can implement certain design patterns in LangGraph. If the pattern suits your needs, we recommend combining it with some of the other fundamental patterns described elsewhere in the docs for best performance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0d7b6dcc-c985-46e2-8457-7e6b0298b950", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U langchain langchain_openai langsmith pandas" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "743c19df-6da9-4d1e-b2d2-ea40080b9fdc", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "075c91c3-c249-471d-b259-41975faa83fb", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "5e4344a7-21df-4d54-90d2-9d19b3416ffb", + "metadata": {}, + "source": [ + "## Create graph utilites\n", + "\n", + "The following helper functions will simplify the code when it comes to actually constructing the graph.\n", + "\n", + "You can skip ahead if you just want to see what the graph looks like." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4325a10e-38dc-4a98-9004-e1525eaba377", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "from langchain_core.messages import (\n", + " AIMessage,\n", + " BaseMessage,\n", + " ChatMessage,\n", + " FunctionMessage,\n", + " HumanMessage,\n", + ")\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation\n", + "\n", + "\n", + "def add_agent_node(workflow: StateGraph, name: str, llm, tools, system_message: str):\n", + " \"\"\"Create an agent and add it to the graph builder.\"\"\"\n", + " functions = [format_tool_to_openai_function(t) for t in tools]\n", + "\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful AI assistant, collaborating with other assistants.\"\n", + " \" Use the provided tools to progress towards answering the question.\"\n", + " \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n", + " \" will help where you left off. Execute what you can to make progress.\"\n", + " \" If you or any of the other assistants have the final answer or deliverable,\"\n", + " \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n", + " \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + " ).partial(system_message=system_message)\n", + "\n", + " chain = (\n", + " (lambda x: {**x, \"intermediate_steps\": []})\n", + " | prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n", + " | llm.bind_functions(functions)\n", + " | (lambda x: _update_state(x, name=name))\n", + " )\n", + " workflow.add_node(name, chain)\n", + "\n", + "\n", + "def _update_state(ai_message, name: str) -> dict:\n", + " \"\"\"This is called after each worker agent is invoked.\n", + "\n", + " It is used to update the global graph state using the agent output.\"\"\"\n", + " if isinstance(ai_message, FunctionMessage):\n", + " result = ai_message\n", + " else:\n", + " result = HumanMessage(**ai_message.dict(exclude={\"type\", \"name\"}), name=name)\n", + " return {\n", + " \"messages\": [result],\n", + " # Since we have a strict workflow, we can\n", + " # track the sender so we know who to pass to next.\n", + " \"sender\": name,\n", + " }\n", + "\n", + "\n", + "def call_tool(state, tool_executor):\n", + " \"\"\"This runs tools in the graph\n", + "\n", + " It takes in an agent action and calls that tool and returns the result.\"\"\"\n", + " messages = state[\"messages\"]\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_input = json.loads(\n", + " last_message.additional_kwargs[\"function_call\"][\"arguments\"]\n", + " )\n", + " # We can pass single-arg inputs by value\n", + " if len(tool_input) == 1 and \"__arg1\" in tool_input:\n", + " tool_input = next(iter(tool_input.values()))\n", + " tool_name = last_message.additional_kwargs[\"function_call\"][\"name\"]\n", + " action = ToolInvocation(\n", + " tool=tool_name,\n", + " tool_input=tool_input,\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(\n", + " content=f\"{tool_name} response: {str(response)}\", name=action.tool\n", + " )\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "f1b54c0c-0b09-408b-abc5-86308929afb6", + "metadata": {}, + "source": [ + "## Create graph\n", + "\n", + "Now that we've defined our tools and made some helper functions, will create the individual agents below and tell them how to talk to each other using LangGraph." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4dce3901-6ad5-4df5-8528-6e865cf96cb0", + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, List, Sequence, Tuple, TypedDict, Union\n", + "\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain.tools.render import format_tool_to_openai_function\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.tools import tool\n", + "from langchain_experimental.utilities import PythonREPL\n", + "from langchain_openai import ChatOpenAI\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "# This defines the object that is passed between each node\n", + "# in the graph. We will create different nodes for each agent and tool\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + " sender: str\n", + "\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", + "\n", + "repl = PythonREPL()\n", + "\n", + "\n", + "@tool\n", + "def python_repl(\n", + " code: Annotated[str, \"The python code to execute to generate your chart.\"]\n", + "):\n", + " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", + " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", + " try:\n", + " result = repl.run(code)\n", + " except BaseException as e:\n", + " return f\"Failed to execute. Error: {repr(e)}\"\n", + " return f\"Succesfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n", + "\n", + "\n", + "workflow = StateGraph(AgentState)\n", + "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", + "add_agent_node(\n", + " workflow,\n", + " \"Researcher\",\n", + " llm,\n", + " [tavily_tool],\n", + " system_message=\"You should provide accurate data for the chart generator to use.\",\n", + ")\n", + "add_agent_node(\n", + " workflow,\n", + " \"Chart Generator\",\n", + " llm,\n", + " [python_repl],\n", + " system_message=\"Any charts you display will be visible by the user.\",\n", + ")\n", + "\n", + "# The \"tool executor\" node is called whenever any worker agent\n", + "#\n", + "tools = [tavily_tool, python_repl]\n", + "tool_executor = ToolExecutor(tools)\n", + "workflow.add_node(\"call_tool\", lambda state: call_tool(state, tool_executor))\n", + "\n", + "\n", + "# Either agent can decide to end\n", + "def router(state):\n", + " # This is the router\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " if \"function_call\" in last_message.additional_kwargs:\n", + " # The previus agent is invoking a tool\n", + " return \"call_tool\"\n", + " if \"FINAL ANSWER\" in last_message.content:\n", + " # Any agent decided the work is done\n", + " return \"end\"\n", + " return \"continue\"\n", + "\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"Researcher\",\n", + " router,\n", + " {\"continue\": \"Chart Generator\", \"call_tool\": \"call_tool\", \"end\": END},\n", + ")\n", + "workflow.add_conditional_edges(\n", + " \"Chart Generator\",\n", + " router,\n", + " {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"end\": END},\n", + ")\n", + "# We will assume that any time a tool is called, the researcher will choose what to do next\n", + "workflow.add_conditional_edges(\n", + " \"call_tool\",\n", + " # Each agent node updates the 'sender' field\n", + " # the tool calling node does not, meaning\n", + " # this edge will route back to the original agent\n", + " # who invoked the tool\n", + " lambda x: x[\"sender\"],\n", + " {\n", + " \"Researcher\": \"Researcher\",\n", + " \"Chart Generator\": \"Chart Generator\",\n", + " },\n", + ")\n", + "workflow.set_entry_point(\"Researcher\")\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "8c9447e7-9ab6-43eb-8ae6-9b52f8ba8425", + "metadata": {}, + "source": [ + "## Invoke\n", + "\n", + "With the graph created, you can invoke it! Let's have it chart some stats for us." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "176a99b0-b457-45cf-8901-90facaa852da", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Python REPL can execute arbitrary code. Use with caution.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1cAAAHWCAYAAACbsXOkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB0rElEQVR4nO3dd3gU5f7+8Xs3vYcAAUIJoXcIzYI0gSBSREUElOZRUZqIKILSpYoKiIDliEc6ioiVTkAUUEKv0nuAUBIgPTu/P/glX2MCJGGTSXm/rovrnJ19ZuaezZNxP5lnnrEYhmEIAAAAAHBfrGYHAAAAAID8gOIKAAAAAOyA4goAAAAA7IDiCgAAAADsgOIKAAAAAOyA4goAAAAA7IDiCgAAAADsgOIKAAAAAOyA4goAAAAA7IDiCgAKoNDQUFksFn377bdmR0EusXTpUvn5+enmzZum5mjWrJmaNWuWrds8efKkLBaLvvrqK7vuJz+5cuWKPDw89Msvv5gdBchTKK4ApDF69GhZLBZFRESk+36NGjXS/aIyderUVO0Mw1CfPn1ksVg0evToe+43Li5OH3/8sR555BEVKlRIzs7OCggIUIcOHbRo0SIlJSWl2WfyPycnJxUpUkQPP/ywhg8frtOnT6fZfnJB8c91ypUrpx49euj48eMZ+mwSEhI0Y8YMNWjQQF5eXvL09FSDBg00Y8YMJSQkZGgbOWnhwoWaNm2aafv/98/JwcFBZcqU0ZNPPqldu3Zlyz4nTJig77//PsPt/5nvn/8mTZp01/XatGmjQoUK6eLFi2nei4yMVIkSJfTAAw/IZrNl9hByXFJSkkaNGqUBAwbI09MzZXnZsmXv+Pk89thjJibOuw4dOqS33npLderUkZeXl0qUKKG2bdtq+/bt6bY/d+6cOnfuLF9fX3l7e+uJJ55Ic746c+aMxowZo4YNG6pQoUIqUqSImjVrprVr16a7zevXr+vll19W0aJF5eHhoebNm2vHjh2p2hQuXFgvvviiRowYYZ8DBwoIR7MDAMifDMNQ37599dlnn2nEiBH3LK4uX76sNm3aKCwsTK1bt9a7774rPz8/hYeHa+3aterWrZuOHj2a5j/0Xbt21eOPPy6bzaZr167pr7/+0rRp0zR9+nT997//VZcuXdLsa+DAgWrQoIESEhK0Y8cOffbZZ/r555+1d+9eBQQE3DHjrVu31LZtW23cuFHt2rVTr169ZLVatXLlSr322mv67rvv9PPPP8vDwyNLn1l2WLhwofbt26dBgwaZmiP555SUlKSDBw9q9uzZ+vXXX7V161bVqVPHrvuaMGGCOnXqpI4dO2Z4nVatWqlHjx6plgUHB991nVmzZqlGjRp6/fXXtXDhwlTvDR8+XBEREVq5cqWs1tz/d8wff/xRhw8f1ssvv5zmvTp16uiNN95Is/xuvyu5XWBgoGJiYuTk5JTj+/7iiy/03//+V08//bT69u2ryMhIffrpp3rwwQe1cuVKtWzZMqXtzZs31bx5c0VGRmr48OFycnLSRx99pKZNm2rXrl0qXLiwJGnFihWaPHmyOnbsqJ49eyoxMVFff/21WrVqpS+//FK9e/dO2abNZlPbtm21e/duvfnmmypSpIhmzZqlZs2aKSwsTBUrVkxp+8orr2jGjBlav369Hn300Zz7kIC8zACAfxk1apQhybh8+XK671evXt1o2rRpyusTJ04Ykoz3338/ZVm/fv0MScY777yToX22bt3asFqtxrJly9J9/6+//jLmz59/130mO3nypFGpUiXD2dnZ2LVrV8ryDRs2GJKMb775JlX7GTNmGJKMCRMm3DXjyy+/bEgyPv744zTvzZw505BkvPLKK3fdhr3ZbDYjOjr6ju+3bdvWCAwMTLP8Tp+Fvd3p5/TDDz8YkoyXX37Z7vv08PAwevbsmeH2kox+/fplaV+TJ082JBmrVq1KWfbnn38aVqvVeOutt7K0zcyKiYkxkpKS7msbHTp0MB555JE0ywMDA422bdve17Yzq2nTpqnOL7l1m1m1fft248aNG6mWRUREGEWLFjUaNWqUanly//rzzz9Tlh08eNBwcHAwhg0blrJs3759ac7XsbGxRpUqVYxSpUqlWr5kyZI0v/uXLl0yfH19ja5du6bJW6NGDaN79+6ZP1CggMr9f04DkOe89tpr+uSTTzRs2DC9995792y/ZcsWrVq1Si+//LKeeuqpdNvUr19fzz33XIb2HxgYqK+++krx8fGaMmXKPdsn/0X2xIkTd2xz9uxZ/fe//9Wjjz6q/v37p3m/X79+at68ub744gudPXtW0u3hk82bN0/T1mazqWTJkurUqVOqZdOmTVP16tXl6uqqYsWKqU+fPrp27VqqdcuWLat27dpp1apVql+/vtzc3PTpp5+mm7lZs2b6+eefderUqZShXGXLlk2TZfz48SpVqpRcXV3VokULHT16NM22tm3bpscee0w+Pj5yd3dX06ZN9fvvv9/x87qXf3/mK1asUNu2bRUQECAXFxeVL19e48aNSzUUVJKOHDmip59+WsWLF5erq6tKlSqlLl26KDIyUtLtIX63bt3S//73v5Rj7tWrV4YyxcTEKDY2NlPHMXjwYNWqVUt9+/ZVbGyskpKS9MorrygwMFCjRo2SdHsYWKdOneTn5ydXV1fVr19fP/zwQ6rtXL16VUOGDFHNmjXl6ekpb29vtWnTRrt3707VLnlo6+LFi/Xuu++qZMmScnd3V1RUlBISEjRmzBhVrFhRrq6uKly4sB555BGtWbPmrscQGxub5opJZly6dElFixZVs2bNZBhGyvKjR4/Kw8NDzz77bKr28+fPV8OGDeXu7q5ChQqpSZMmWr169R23/9VXX8lisejkyZOplid/FqGhoamWf/bZZypfvrzc3NzUsGFD/fbbb2m2md49V7169ZKnp6fOnTunjh07ytPTU0WLFtWQIUPS9MMrV66oe/fu8vb2lq+vr3r27Kndu3dn6D6uevXqpRp6Kd0egte4cWMdPHgw1fJvv/1WDRo0UIMGDVKWValSRS1atNDSpUtTllWvXl1FihRJta6Li4sef/xxnT17Vjdu3Ei1zWLFiqU61xYtWlSdO3fWihUrFBcXl2o7rVq10o8//pjqZwvgziiuANjV66+/rhkzZmjo0KGaMGFChtb58ccfJUnPP/+83XI89NBDKl++/D2/WErSsWPHJClliE16fv31VyUlJaUZOvZPPXr0UGJiolauXClJevbZZ7Vp0yaFh4enard582adP38+1ZDFPn366M0331SjRo00ffp09e7dWwsWLFDr1q3T3Mt1+PBhde3aVa1atdL06dPvOKzunXfeUZ06dVSkSBHNmzdP8+bNS3P/1aRJk7R8+XINGTJEw4YN09atW9MUsevXr1eTJk0UFRWlUaNGacKECbp+/boeffRR/fnnn3f8PO7m35/5V199JU9PTw0ePFjTp09XvXr1NHLkSL399tsp68THx6t169baunWrBgwYoE8++UQvv/yyjh8/ruvXr0uS5s2bJxcXFzVu3DjlmPv06XPPPF999ZU8PDzk5uamatWqpRnmdyeOjo767LPPdOLECY0bN04zZ87Ujh07NHv2bLm7u2v//v168MEHdfDgQb399tv64IMP5OHhoY4dO2r58uUp2zl+/Li+//57tWvXTh9++KHefPNN7d27V02bNtX58+fT7HfcuHH6+eefNWTIEE2YMEHOzs4aPXq0xowZo+bNm2vmzJl65513VKZMmTT30vxbWFiY4uPjVbdu3XTfT0hIUERERJp/MTExkiR/f3/Nnj1bGzdu1McffyzpdtHeq1cveXl5adasWSnbGjNmjLp37y4nJyeNHTtWY8aMUenSpbV+/foMfd738t///ld9+vRR8eLFNWXKFDVq1EgdOnTQmTNnMrR+UlKSWrdurcKFC2vq1Klq2rSpPvjgA3322WcpbWw2m9q3b69FixapZ8+eGj9+vC5cuKCePXveV/bw8PBUBZLNZtOePXtUv379NG0bNmyoY8eOpSqa7rRNd3d3ubu7pyzbuXOn6tatm2a4asOGDRUdHa2///471fJ69erp+vXr2r9/f1YOCyh4zL50BiD3yeqwwMDAQEOS8eabb2Zqf08++aQhybh+/Xqq5TExMcbly5dT/l27di3NPtMbFpjsiSeeMCQZkZGRhmH831C4L7/80rh8+bJx/vx54+effzbKli1rWCwW46+//rrjtgYNGmRIMnbu3HnHNjt27DAkGYMHDzYMwzAOHz6c7jDCvn37Gp6eninD+X777TdDkrFgwYJU7VauXJlmefJnvHLlyjvm+Kd7DQusWrWqERcXl7J8+vTphiRj7969hmHcHnZYsWJFo3Xr1obNZktpFx0dbQQFBRmtWrW66/6Tf05jxowxLl++bISHhxuhoaFGcHCwISllGGh6Qxv79OljuLu7G7GxsYZhGMbOnTszNJQxs8MCH374YWPatGnGihUrjNmzZxs1atQwJBmzZs3K8Db69+9vODk5GZ6enqmGVrVo0cKoWbNmyjEYxu3P9OGHHzYqVqyYsiw2NjbN0L4TJ04YLi4uxtixY1OWJf/cypUrl+Yzq127dpaG8H3xxRepfub/lNzf0vs3ceLEVG27du1quLu7G3///bfx/vvvG5KM77//PuX9I0eOGFar1XjyySfTHOs/+9a/h/DNnTvXkGScOHEi1TrJn8WGDRsMwzCM+Ph4w9/f36hTp06qPv3ZZ58ZktI9Z82dOzdlWc+ePQ1JqT5vwzCM4OBgo169eimv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/aVly5bJYrFo3rx5Gb7fJCM++ugjHTp0SH379tXKlStTrlCtWrVKK1asSJm2+d86d+6sIUOGaMiQIfLz80tzFaNp06bq06ePJk6cqF27dikkJEROTk46cuSIvvnmG02fPj3D96/9W7169bRkyRINHjxYDRo0kKen5z1/dv9ktVr1xRdfqE2bNqpevbp69+6tkiVL6ty5c9qwYYO8vb1TptLPqocffliFChVSz549NXDgwJSf3b+/cK9fv179+/fXM888o0qVKikxMVHz5s2Tg4ODnn766VTHvHbtWn344YcKCAhQUFCQHnjggXT3/cknn+j7779X+/btVaZMGV24cEFffvmlTp8+rXnz5qWaGCErPvnkEz3yyCOqWbOmXnrpJZUrV04XL17Uli1bdPbs2ZTnWLVr105jx45V79699fDDD2vv3r1asGBBhu8Vkm4PR23WrJnq1asnPz8/bd++Xd9++226z2X7J1dXV4WEhGjt2rUaO3ZsmvfPnTun+fPnp1nu6empjh07Srr9bLsrV65o7dq1cnBw0GOPPaYXX3xR7733np544gnVrl1bFSpU0DvvvKNx48apcePGeuqpp+Ti4qK//vpLAQEBmjhxYrr5qlevrgcffFDDhg3T1atX5efnp8WLF6cptJ2cnPTee++pT58+evTRR/Xss8/qxIkTmjt3bqY+x3vp2LGjGjZsqDfeeENHjx5VlSpV9MMPP6QMIb3XldNp06Zp1qxZeuihh+Tu7p7ms33yySfl4eEhSerbt68+//xztW3bVkOGDJGTk5M+/PBDFStWTG+88UbKOsuXL9dbb72lihUrqmrVqmm22apVq5R7ATt16qQHH3xQvXv31oEDB1SkSBHNmjVLSUlJGjNmTJq8a9asUfv27bnnCsgo0+YpBJDrzZ8/33jwwQcNDw8Pw8XFxahSpYoxZsyYVNNKG8bdp0X/7bffDDc3NyMoKMg4d+7cXfcXExNjTJs2zXjooYcMb29vw9HR0ShevLjRrl07Y8GCBUZiYmKafSb/c3R0NPz8/IwHHnjAGDZsmHHq1Kk020+euvleU3nfTVxcnPHRRx8Z9erVMzw8PAx3d3ejbt26xrRp04z4+Pg7rteoUSNDkvHiiy/esc1nn31m1KtXz3BzczO8vLyMmjVrGm+99ZZx/vz5lDaBgYGZmm775s2bRrdu3QxfX9+U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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1cAAAHWCAYAAACbsXOkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAACJIUlEQVR4nOzdd1gU58IF8LO79N6lF6mCvXexYSemR5MYTXLTNGr8TNGogCZqTFNjoukmUdOLHcWCvcQWBQRpiiAd6W3Zne8PZCMRdMGF2YXzex6em52Z3Tm7vBc5zMw7EkEQBBAREREREdF9kYodgIiIiIiIqC1guSIiIiIiItIAlisiIiIiIiINYLkiIiIiIiLSAJYrIiIiIiIiDWC5IiIiIiIi0gCWKyIiIiIiIg1guSIiIiIiItIAlisiIiIiIiINYLkiIiJqQHR0NCQSCX777TexoxARkY5guSIiaobw8HBIJBLk5eU1uL5z584IDg5WPb569SokEgk++OCDetsJgoAXX3wREokE4eHh99xvVVUVPvnkEwwePBjW1tYwMDCAs7MzQkND8eOPP0KhUNyxz7ovfX192NnZYeDAgVi4cCHS0tLueP26QnH7czp27Ihp06YhJSVFrc9GLpdj7dq16NOnD8zNzWFmZoY+ffpg7dq1kMvlar1Ga9qyZQtWr14t2v7/+32SyWRwd3fHgw8+iAsXLrTIPpcvX46//vpL7e1vz3f718qVK+/6vHHjxsHa2hrZ2dl3rCsqKoKTkxP69esHpVLZ1LdARKSV9MQOQETUXgmCgFdeeQVffPEFFi9efM9ylZubi3HjxuHs2bMYM2YMFi1aBBsbG2RlZWHfvn2YOnUqkpKSsHjx4nrPmzJlCsaPHw+lUombN2/i77//xurVq7FmzRp8/fXXeOKJJ+7Y1+zZs9GnTx/I5XKcO3cOX3zxBXbu3IlLly7B2dm50YxlZWWYMGECDh06hIkTJ2L69OmQSqWIjIzEnDlz8Mcff2Dnzp0wNTVt1mfWErZs2YKYmBjMnTtX1Bx13yeFQoHLly9j/fr12L17N06ePInu3btrdF/Lly/HI488gsmTJ6v9nNGjR2PatGn1lvXo0eOuz/nss8/QuXNnvPbaa9iyZUu9dQsXLkReXh4iIyMhlfJvvUTURghERNRkYWFhAgAhNze3wfVBQUHCsGHDVI9TU1MFAML777+vWjZz5kwBgPD222+rtc8xY8YIUqlU+P333xtc//fffwubNm266z7rXL16VfDz8xMMDAyECxcuqJYfPHhQACD8+uuv9bZfu3atAEBYvnz5XTO+8MILAgDhk08+uWPdunXrBADCSy+9dNfX0DSlUimUl5c3un7ChAmCh4fHHcsb+yw0rbHv07Zt2wQAwgsvvKDxfZqamgrPPPOM2tsDEGbOnNmsfb333nsCAGHPnj2qZadPnxakUqnwxhtvNOs1m6qiokJQKBStsi8iat/4pyIiIhHMmTMHn376KRYsWIB33nnnntufOHECe/bswQsvvICHHnqowW169+6NJ598Uq39e3h4YOPGjaiursaqVavuuf2IESMAAKmpqY1uk56ejq+//hojRozArFmz7lg/c+ZMDB8+HF999RXS09MB1J4+OXz48Du2VSqVcHFxwSOPPFJv2erVqxEUFAQjIyN06NABL774Im7evFnvuZ6enpg4cSL27NmD3r17w9jYGJ9//nmDmYODg7Fz505cu3ZNdaqbp6fnHVneffdduLq6wsjICCNHjkRSUtIdr3Xq1CmMHTsWlpaWMDExwbBhw3Ds2LFGP697+e9nvnXrVkyYMAHOzs4wNDSEt7c3li1bVu9UUABITEzEww8/DEdHRxgZGcHV1RVPPPEEioqKANSe4ldWVobvvvtO9Z6nT5+uVqaKigpUVlY26X3MmzcPXbt2xSuvvILKykooFAq89NJL8PDwQFhYGAAgPj4ejzzyCGxsbGBkZITevXtj27Zt9V6noKAA8+fPR5cuXWBmZgYLCwuMGzcO//zzT73t6k5t/emnn7Bo0SK4uLjAxMQExcXFkMvliIiIgK+vL4yMjGBra4vBgwcjKiqqSe+JiKgxPC2QiKiVvfbaa1i7di3efPNNLF++XK3nbN++HQDw1FNPaSzHgAED4O3trdYvlsnJyQAAW1vbRrfZvXs3FArFHaeO3W7atGk4ePAgIiMj8fzzz+Pxxx9HeHg4srKy4OjoqNru6NGjuHHjRr1TFl988UVs3LgRM2bMwOzZs5Gamop169bh/PnzOHbsGPT19VXbJiQkYMqUKXjxxRfxv//9D/7+/g3mefvtt1FUVIT09HR8/PHHAAAzM7N626xcuRJSqRTz589HUVERVq1ahSeffBKnTp1SbXPgwAGMGzcOvXr1QlhY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/suwfv16FBUVITg4GE5OTqqvn3/+WbVNWloaMjMzVY8HDhyILVu24IsvvkC3bt3w22+/4a+//qo3CcYbb7yBV199FS+88AL69OmD0tJSREZGwsjIqFXfHxER6YZeHtZ4qKcLACBsWyyUSqHeekEQEL4tFgqlgDFBHTDE116MmEREpOVEveZKEIR7bhMdHX3HskcffRSPPvpoo8+RSCRYunQpli5dej/xiIioHXlrbAD2xGThn+u106w/2ttNtW5PXA6OJ+fDQE+KRRMCRUxJRETajOc0EBERAXCwMMLskb4Aam8QfLO8GqdSC3AqR4Lw7XEAgJeGdoSbjXbcooOIiLSPVswWSEREpA1mDPLCz39fR0peGQavPICyagUAGQA5pBLA24GzAxIRUeN45IqIiOgWAz0pxnepvaVHbbH6l1IA5v50AZExmQ09lYiIiOWKiIiojkIp4PdzGXfdJmJ7HBTKe18zTERE7Q/LFRER0S2nUwuQWVTZ6HoBQGZRJU6nFrReKCIi0hksV0RERLfklDRerJqzHRERtS8sV0RERLc4mKt3P0R1tyMiovaF5YqIiOiWvl42cLI0gqSR9RIATpZG6Otl05qxiIhIR7BcERER3SKTShA2qfYmwf8tWHWPwyYFQiZtrH4REVF7xnJFRER0m7GdnbD+qZ5wtKx/6p+jpRHWP9UTYzs7iZSMiIi0HW8iTERE9B9jOzthdKAjTiTlYO+RUwgZ0g8DfBx4xIqIiO6K5YqIiKgBMqkE/bxskH9ZQD8vGxYrIiK6J54WSEREREREpAEsV0RERERERBrAckVERERERKQBLFdEREREREQawHJFRERERESkASxXREREREREGsByRUREREREpAEsV0RERERERBrAckVERERERKQBLFdEREREREQaoCd2AG0kCAIAoLi4WOQkgFwuR3l5OYqLi6Gvry92HKIm4fglXccxTLqM45d0mTaN37pOUNcR7oblqgElJSUAADc3N5GTEBERERGRNigpKYGlpeVdt5EI6lSwdkapVOLGjRswNzeHRCIRNUtxcTHc3Nxw/fp1WFhYiJqFqKk4fknXcQyTLuP4JV2mTeNXEASUlJTA2dkZUundr6rikasGSKVSuLq6ih2jHgsLC9EHFlFzcfySruMYJl3G8Uu6TFvG772OWNXhhBZEREREREQawHJFRERERESk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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "HumanMessage(content=\"FINAL ANSWER\\n\\nThe complete line graph of the UK's GDP from 2018 to 2022 is displayed above, with the following GDP values for each year:\\n\\n- 2018: 2.16 trillion GBP\\n- 2019: 2.23 trillion GBP\\n- 2020: 1.99 trillion GBP\\n- 2021: 2.23 trillion GBP\\n- 2022: 2.27 trillion GBP\\n\\nThe graph shows the fluctuations in the UK's GDP over the specified period, with a notable dip in 2020 likely due to the economic impact of the COVID-19 pandemic.\", name='Researcher')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = graph.invoke(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Fetch the UK's GDP over the past 5 years,\"\n", + " \" then draw a line graph of it.\"\n", + " \" Once you code it up, finish.\"\n", + " )\n", + " ],\n", + " },\n", + " # Maximum number of steps to take in the graph\n", + " {\"recursion_limit\": 150},\n", + ")\n", + "result[\"messages\"][-1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23cc22c7-33a8-4399-91e4-ac1abf8f0fec", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langgraph/graph/message.py b/langgraph/graph/message.py new file mode 100644 index 000000000..d41a2e747 --- /dev/null +++ b/langgraph/graph/message.py @@ -0,0 +1,24 @@ +from typing import Annotated, Union + +from langchain_core.messages import AnyMessage + +from langgraph.graph.state import StateGraph + +Messages = Union[list[AnyMessage], AnyMessage] + + +def add_messages(left: Messages, right: Messages) -> Messages: + if not isinstance(left, list): + left = [left] + if not isinstance(right, list): + right = [right] + return left + right + + +class MessageGraph(StateGraph): + """A StateGraph where every node + - receives a list of messages as input + - returns one or more messages as output.""" + + def __init__(self) -> None: + super().__init__(Annotated[list[AnyMessage], add_messages]) diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index 1a3f5530e..a11288d91 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -3,7 +3,7 @@ from functools import partial from inspect import signature from typing import Any, Optional, Type -from langchain_core.runnables import RunnableConfig, RunnableLambda +from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate @@ -32,6 +32,17 @@ class StateGraph(Graph): raise ValueError("Cannot use channel names as node names") state_keys = list(self.channels) + state_keys_read = state_keys[0] if state_keys == ["__root__"] else state_keys + update_state = ( + _update_state_dict + if isinstance(state_keys_read, list) + else _update_state_root + ) + coerce_state = ( + partial(_coerce_state, self.schema) + if isinstance(state_keys_read, list) + else RunnablePassthrough() + ) outgoing_edges = defaultdict(list) for start, end in self.edges: @@ -40,9 +51,9 @@ class StateGraph(Graph): nodes = { key: ( Channel.subscribe_to(f"{key}:inbox") - | partial(_coerce_state, self.schema) # coerce/validate using schema + | coerce_state # coerce/validate using schema | node - | _update_state + | update_state | Channel.write_to(key) ) for key, node in self.nodes.items() @@ -54,7 +65,7 @@ class StateGraph(Graph): if outgoing or key in self.branches: nodes[edges_key] = Channel.subscribe_to( key, tags=["langsmith:hidden"] - ) | ChannelRead(state_keys) + ) | ChannelRead(state_keys_read) if outgoing: nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) if key in self.branches: @@ -65,12 +76,12 @@ class StateGraph(Graph): nodes[START] = ( Channel.subscribe_to(f"{START}:inbox", tags=["langsmith:hidden"]) - | _update_state + | update_state | Channel.write_to(START) ) nodes[f"{START}:edges"] = ( Channel.subscribe_to(START, tags=["langsmith:hidden"]) - | ChannelRead(state_keys) + | ChannelRead(state_keys_read) | Channel.write_to(f"{self.entry_point}:inbox") ) @@ -88,26 +99,37 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]: return schema(**input) -def _update_state(input: dict[str, Any], config: RunnableConfig) -> dict[str, Any]: +def _update_state_dict(input: dict[str, Any], config: RunnableConfig) -> dict[str, Any]: if input is not None: ChannelWrite.do_write(config, **input) return input +def _update_state_root(input: Any, config: RunnableConfig) -> dict[str, Any]: + if input is not None: + ChannelWrite.do_write(config, __root__=input) + return input + + def _get_channels(schema: Type[dict]) -> dict[str, BaseChannel]: if not hasattr(schema, "__annotations__"): - raise ValueError("Schema must be a class with type annotations") + return { + "__root__": _get_channel(schema), + } channels: dict[str, BaseChannel] = {} for name, typ in schema.__annotations__.items(): - if channel := _is_field_binop(typ): - channels[name] = channel - else: - channels[name] = LastValue(typ) + channels[name] = _get_channel(typ) return channels +def _get_channel(annotation: Any) -> Optional[BaseChannel]: + if channel := _is_field_binop(annotation): + return channel + return LastValue(annotation) + + def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: if hasattr(typ, "__metadata__"): meta = typ.__metadata__ diff --git a/pyproject.toml b/pyproject.toml index 5dae9d362..c4802162d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.0.15" +version = "0.0.16" description = "langgraph" authors = [] license = "LangGraph License" diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 1dc57988e..205c34d23 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -1,3 +1,4 @@ +import json import operator import time import warnings @@ -16,7 +17,10 @@ from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, Graph +from langgraph.graph.message import MessageGraph from langgraph.graph.state import StateGraph +from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor +from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -788,8 +792,6 @@ def test_conditional_graph() -> None: def test_conditional_graph_state() -> None: - from copy import deepcopy - from langchain.llms.fake import FakeStreamingListLLM from langchain_community.tools import tool from langchain_core.agents import AgentAction, AgentFinish @@ -894,7 +896,7 @@ def test_conditional_graph_state() -> None: ), } - assert [deepcopy(c) for c in app.stream({"input": "what is weather in sf"})] == [ + assert [*app.stream({"input": "what is weather in sf"})] == [ { "agent": { "agent_outcome": AgentAction( @@ -973,3 +975,324 @@ def test_conditional_graph_state() -> None: } }, ] + + +def test_prebuilt_chat() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + class FakeFuntionChatModel(FakeMessagesListChatModel): + def bind_functions(self, functions: list): + return self + + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + app = create_function_calling_executor( + FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("query"), + } + }, + ), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("another"), + } + }, + ), + AIMessage(content="answer"), + ] + ), + tools, + ) + + assert app.invoke( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) == { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + + assert [ + *app.stream({"messages": [HumanMessage(content="what is weather in sf")]}) + ] == [ + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"query"', + } + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for query", name="search_api") + ] + } + }, + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for another", name="search_api") + ] + } + }, + {"agent": {"messages": [AIMessage(content="answer")]}}, + { + "__end__": { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"query"', + } + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + }, + ] + + +def test_message_graph() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.agents import AgentAction + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + class FakeFuntionChatModel(FakeMessagesListChatModel): + def bind_functions(self, functions: list): + return self + + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + model = FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("query"), + } + }, + ), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("another"), + } + }, + ), + AIMessage(content="answer"), + ] + ) + + tool_executor = ToolExecutor(tools) + + # Define the function that determines whether to continue or not + def should_continue(messages): + last_message = messages[-1] + # If there is no function call, then we finish + if "function_call" not in last_message.additional_kwargs: + return "end" + # Otherwise if there is, we continue + else: + return "continue" + + def call_tool(messages): + # Based on the continue condition + # we know the last message involves a function call + last_message = messages[-1] + # We construct an AgentAction from the function_call + action = AgentAction( + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads( + last_message.additional_kwargs["function_call"]["arguments"] + ), + log="", + ) + # We call the tool_executor and get back a response + response = tool_executor.invoke(action) + # We use the response to create a FunctionMessage + return FunctionMessage(content=str(response), name=action.tool) + + # Define a new graph + workflow = MessageGraph() + + # Define the two nodes we will cycle between + workflow.add_node("agent", model) + workflow.add_node("action", call_tool) + + # Set the entrypoint as `agent` + # This means that this node is the first one called + workflow.set_entry_point("agent") + + # We now add a conditional edge + workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END, + }, + ) + + # We now add a normal edge from `tools` to `agent`. + # This means that after `tools` is called, `agent` node is called next. + workflow.add_edge("action", "agent") + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + app = workflow.compile() + + assert app.invoke(HumanMessage(content="what is weather in sf")) == [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + + assert [*app.stream([HumanMessage(content="what is weather in sf")])] == [ + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ) + }, + {"action": FunctionMessage(content="result for query", name="search_api")}, + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ) + }, + {"action": FunctionMessage(content="result for another", name="search_api")}, + {"agent": AIMessage(content="answer")}, + { + "__end__": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + }, + ] diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 1b162cbb8..286db9a3e 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -1,4 +1,5 @@ import asyncio +import json import operator from contextlib import asynccontextmanager, contextmanager from typing import ( @@ -23,6 +24,9 @@ from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, Graph, StateGraph +from langgraph.graph.message import MessageGraph +from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor +from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -834,8 +838,6 @@ async def test_conditional_graph() -> None: async def test_conditional_graph_state() -> None: - from copy import deepcopy - from langchain.llms.fake import FakeStreamingListLLM from langchain_community.tools import tool from langchain_core.agents import AgentAction, AgentFinish @@ -940,9 +942,7 @@ async def test_conditional_graph_state() -> None: ), } - assert [ - deepcopy(c) async for c in app.astream({"input": "what is weather in sf"}) - ] == [ + assert [c async for c in app.astream({"input": "what is weather in sf"})] == [ { "agent": { "agent_outcome": AgentAction( @@ -1021,3 +1021,329 @@ async def test_conditional_graph_state() -> None: } }, ] + + +async def test_prebuilt_chat() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + class FakeFuntionChatModel(FakeMessagesListChatModel): + def bind_functions(self, functions: list): + return self + + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + app = create_function_calling_executor( + FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("query"), + } + }, + ), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("another"), + } + }, + ), + AIMessage(content="answer"), + ] + ), + tools, + ) + + assert await app.ainvoke( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) == { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + + assert [ + c + async for c in app.astream( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) + ] == [ + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"query"', + } + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for query", name="search_api") + ] + } + }, + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for another", name="search_api") + ] + } + }, + {"agent": {"messages": [AIMessage(content="answer")]}}, + { + "__end__": { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"query"', + } + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + }, + ] + + +async def test_message_graph() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.agents import AgentAction + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + class FakeFuntionChatModel(FakeMessagesListChatModel): + def bind_functions(self, functions: list): + return self + + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + model = FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("query"), + } + }, + ), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": json.dumps("another"), + } + }, + ), + AIMessage(content="answer"), + ] + ) + + tool_executor = ToolExecutor(tools) + + # Define the function that determines whether to continue or not + def should_continue(messages): + last_message = messages[-1] + # If there is no function call, then we finish + if "function_call" not in last_message.additional_kwargs: + return "end" + # Otherwise if there is, we continue + else: + return "continue" + + async def call_tool(messages): + # Based on the continue condition + # we know the last message involves a function call + last_message = messages[-1] + # We construct an AgentAction from the function_call + action = AgentAction( + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads( + last_message.additional_kwargs["function_call"]["arguments"] + ), + log="", + ) + # We call the tool_executor and get back a response + response = await tool_executor.ainvoke(action) + # We use the response to create a FunctionMessage + return FunctionMessage(content=str(response), name=action.tool) + + # Define a new graph + workflow = MessageGraph() + + # Define the two nodes we will cycle between + workflow.add_node("agent", model) + workflow.add_node("action", call_tool) + + # Set the entrypoint as `agent` + # This means that this node is the first one called + workflow.set_entry_point("agent") + + # We now add a conditional edge + workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END, + }, + ) + + # We now add a normal edge from `tools` to `agent`. + # This means that after `tools` is called, `agent` node is called next. + workflow.add_edge("action", "agent") + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + app = workflow.compile() + + assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + + assert [ + c async for c in app.astream([HumanMessage(content="what is weather in sf")]) + ] == [ + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ) + }, + {"action": FunctionMessage(content="result for query", name="search_api")}, + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ) + }, + {"action": FunctionMessage(content="result for another", name="search_api")}, + {"agent": AIMessage(content="answer")}, + { + "__end__": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + }, + ]