diff --git a/docs/docs/reference/checkpoints.md b/docs/docs/reference/checkpoints.md
index e93d4d8e5..bf9ba4ad8 100644
--- a/docs/docs/reference/checkpoints.md
+++ b/docs/docs/reference/checkpoints.md
@@ -2,3 +2,33 @@
::: langgraph.checkpoint
handler: python
+
+### BaseCheckpointSaver
+
+::: langgraph.checkpoint.base.BaseCheckpointSaver
+ handler: python
+
+## Implementations
+
+LangGraph also natively provides the following checkpoint implementations.
+
+### AsyncSqliteSaver
+
+::: langgraph.checkpoint.aiosqlite.AsyncSqliteSaver
+ handler: python
+
+
+### SqliteSaver
+
+::: langgraph.checkpoint.sqlite.SqliteSaver
+ handler: python
+ members:
+ - put
+ - list
+ - get_tuple
+
+
+### MemorySaver
+
+::: langgraph.checkpoint.memory.MemorySaver
+ handler: python
\ No newline at end of file
diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md
index f52579368..8704f023b 100644
--- a/docs/docs/reference/graphs.md
+++ b/docs/docs/reference/graphs.md
@@ -1,5 +1,7 @@
# Graph Definitions
+Graphs are the core abstraction of LangGraph. Each [StateGraph](#langgraph.graph.StateGraph) implementation is used to create graph workflows. Once compiled, you can run the [CompiledGraph](#compiledgraph) to run the application.
+
::: langgraph.graph
handler: python
@@ -7,9 +9,6 @@
::: langgraph.graph.graph.CompiledGraph
handler: python
- members:
- - get_graph
- - invoke
## MessageGraph
@@ -19,4 +18,4 @@
## add_messages
-::: ::: langgraph.graph.message.add_messages
\ No newline at end of file
+::: langgraph.graph.message.add_messages
\ No newline at end of file
diff --git a/docs/docs/reference/prebuilt.md b/docs/docs/reference/prebuilt.md
index ebb28b6c6..dd11d90a6 100644
--- a/docs/docs/reference/prebuilt.md
+++ b/docs/docs/reference/prebuilt.md
@@ -37,16 +37,7 @@ from langgraph.prebuilt import ToolInvocation
from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor
```
-::: langgraph.prebuilt.chat_agent_executor
-
-## `create_agent_executor`
-
-```python
-from langgraph.prebuilt import create_agent_executor
-```
-
-::: langgraph.prebuilt.create_agent_executor
-
+::: langgraph.prebuilt.chat_agent_executor.create_tool_calling_executor
## `tools_condition`
diff --git a/examples/agent_executor/high-level.ipynb b/examples/agent_executor/high-level.ipynb
index 2c2ea5a87..d8422acac 100644
--- a/examples/agent_executor/high-level.ipynb
+++ b/examples/agent_executor/high-level.ipynb
@@ -5,344 +5,11 @@
"id": "f961801a-6025-4b73-be3b-c3a8a75d4167",
"metadata": {},
"source": [
- "# Agent Executor\n",
+ "# (Deprecated) Agent Executor\n",
"\n",
- "This notebook walks through an example creating an agent executor to work with an existing LangChain agent.\n",
- "This is useful for getting started quickly.\n",
- "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder."
+ "The `create_agent_executor` function is deprecated in favor of [create_tool_calling_executor](../chat_agent_executor_with_function_calling/high-level-tools.ipynb).\n",
+ "This was done to better align with the underlying model providers' migration from \"function calling\" to \"tool calling\", which typically supports parallel tool usage."
]
- },
- {
- "cell_type": "markdown",
- "id": "e6dd032b-bfe9-458c-a8ef-a14c78e0ad3f",
- "metadata": {},
- "source": [
- "## Setup\n",
- "\n",
- "First we need to install the packages required"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "1759bc06-8af3-4b73-abbf-0be3fa4c31fb",
- "metadata": {},
- "outputs": [],
- "source": [
- "%%capture --no-stderr\n",
- "%pip install --quiet -U langchain langchain_openai tavily-python"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fa08bd1a-efaa-46f5-adf8-47a84f738381",
- "metadata": {},
- "source": [
- "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "eb8e51dc-028b-4ea5-9847-f22fcbed6dac",
- "metadata": {},
- "outputs": [],
- "source": [
- "import os\n",
- "import getpass\n",
- "\n",
- "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
- "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "9242c0d7-b1da-41a0-9a3e-ed3afab3528e",
- "metadata": {},
- "source": [
- "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "5db4438c-7802-4050-9dd9-14a6cac21a91",
- "metadata": {},
- "outputs": [],
- "source": [
- "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
- "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6ae180d9-abd3-4a44-8fb1-a2c89434fbeb",
- "metadata": {},
- "source": [
- "## Set up LangChain Agent\n",
- "\n",
- "First, will set up our LangChain Agent. \n",
- "See documentation [here](https://python.langchain.com/docs/modules/agents/) for more information on what these agents are and how to think about them"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "e2fdcac4-d134-402b-b423-b0cf4b939f5d",
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_openai import ChatOpenAI\n",
- "from langchain import hub\n",
- "from langchain.agents import create_openai_functions_agent\n",
- "from langchain_community.tools.tavily_search import TavilySearchResults"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "afb59979-c7a3-435f-b147-f8d501f6ff13",
- "metadata": {},
- "outputs": [],
- "source": [
- "tools = [TavilySearchResults(max_results=1)]\n",
- "\n",
- "# Get the prompt to use - you can modify this!\n",
- "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
- "\n",
- "# Choose the LLM that will drive the agent\n",
- "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n",
- "\n",
- "# Construct the OpenAI Functions agent\n",
- "agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "0bcb5ff8-b2d1-4fb2-bed4-3726f96db772",
- "metadata": {},
- "source": [
- "## Create agent executor\n",
- "\n",
- "Now we will use the high level method to create the agent executor"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "7a138eb4-a469-4b30-a059-99d6ea944648",
- "metadata": {},
- "outputs": [],
- "source": [
- "from langgraph.prebuilt import create_agent_executor"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "9be722f0-c9ab-4bd2-af27-66adf51134d2",
- "metadata": {},
- "outputs": [],
- "source": [
- "app = create_agent_executor(agent_runnable, tools)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "id": "019b591b-fd71-4ee8-ae94-06d0e2dc6a4d",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n",
- "----\n",
- "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoWeather ☀ ⛅ San Francisco ☀ ⛅ January ☀ ⛅ Information on temperature, sunshine hours, water temperature & rainfall in January for San Francisco. ... Are you planning a holiday with hopefully nice weather in San Francisco in January 2024? Here you can find all information about the weather in San Francisco in January: ... 15. January ...'}]\")]}\n",
- "----\n",
- "{'agent_outcome': AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\")}\n",
- "----\n",
- "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoWeather ☀ ⛅ San Francisco ☀ ⛅ January ☀ ⛅ Information on temperature, sunshine hours, water temperature & rainfall in January for San Francisco. ... Are you planning a holiday with hopefully nice weather in San Francisco in January 2024? Here you can find all information about the weather in San Francisco in January: ... 15. January ...'}]\")]}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n",
- "for s in app.stream(inputs):\n",
- " print(list(s.values())[0])\n",
- " print(\"----\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "id": "c6a664cd-083e-4d85-aeaf-501463881f05",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can check the weather on a reliable weather website or using a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can check the weather on a reliable weather website or using a weather app for the most up-to-date information.\")"
- ]
- },
- "execution_count": 18,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "s[\"__end__\"][\"agent_outcome\"]"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a7bd3e55-ee7e-4276-81bd-39e6131fcf77",
- "metadata": {},
- "source": [
- "## Custom Input Schema\n",
- "\n",
- "By default, the `create_agent_executor` assumes that the input will be a dictionary with two keys: `input` and `chat_history`. \n",
- "If this is not the case, you can easily customize the input schema.\n",
- "You should do this, by defining a schema as a TypedDict.\n",
- "\n",
- "For this example, we will create a new agent that expects `question` and `language` as inputs."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a98c5ec5-f836-4b3c-b37b-00102b496366",
- "metadata": {},
- "source": [
- "### Create New Agent"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "id": "676841ec-b5a6-495e-a88a-7eb0ab3cbae6",
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
- "\n",
- "prompt = ChatPromptTemplate.from_messages(\n",
- " [\n",
- " (\n",
- " \"human\",\n",
- " \"Respond to the user question: {question}. Answer in this language: {language}\",\n",
- " ),\n",
- " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
- " ]\n",
- ")\n",
- "agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "5889d980-d209-447b-8489-1d4873acfdc2",
- "metadata": {},
- "source": [
- "### Define Input Schema"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "id": "3d1df06d-1564-46a1-a72f-58dfc65927bc",
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import TypedDict"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "id": "2fdbb687-9c72-42c7-afcb-3f8940f3e5f4",
- "metadata": {},
- "outputs": [],
- "source": [
- "class InputSchema(TypedDict):\n",
- " question: str\n",
- " language: str"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "329bb518-02a8-477c-8898-d04cb64fc460",
- "metadata": {},
- "source": [
- "### Create new agent executor"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "id": "1ad88990-896d-48d5-bd34-01c9f6a37734",
- "metadata": {},
- "outputs": [],
- "source": [
- "app = create_agent_executor(agent_runnable, tools, input_schema=InputSchema)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "id": "13ffa18c-9a9f-4e0e-8298-32aeff94ce5d",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})])}\n",
- "----\n",
- "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})]), '[{\\'url\\': \\'https://www.mxbars.net/2024/01/14/san-francisco-supercross-2024-results-and-points-video/\\', \\'content\\': \"Scritto domenica 14 Gennaio 2024 alle 04:38. SAN FRANCISCO Oracle Park, CA January 13, 2024 sera gli orari sono anticipati di due ore causa mal tempo per non compromettere lo spettacolo! secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non . Commenta la gara\\\\xa0CLICCANDO\\\\xa0il link! http://forum.mxbars.net/viewtopic.php?f=18&t=50182SAN FRANCISCO. Oracle Park, CA. January 13, 2024. Ecco che la NUOVA stagione del Monster Energy Supercross 2024 continua, dopo Anaheim 1 si passa al secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non vede possibilità di chiudersi, per la lotta nella 450 dove sono tutti agguerriti e quest\\'anno il livello è ancora più alto ...\"}]')]}\n",
- "----\n",
- "{'agent_outcome': AgentFinish(return_values={'output': 'Al momento sta piovendo a San Francisco.'}, log='Al momento sta piovendo a San Francisco.')}\n",
- "----\n",
- "{'question': 'what is the weather in sf', 'language': 'italian', 'agent_outcome': AgentFinish(return_values={'output': 'Al momento sta piovendo a San Francisco.'}, log='Al momento sta piovendo a San Francisco.'), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})]), '[{\\'url\\': \\'https://www.mxbars.net/2024/01/14/san-francisco-supercross-2024-results-and-points-video/\\', \\'content\\': \"Scritto domenica 14 Gennaio 2024 alle 04:38. SAN FRANCISCO Oracle Park, CA January 13, 2024 sera gli orari sono anticipati di due ore causa mal tempo per non compromettere lo spettacolo! secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non . Commenta la gara\\\\xa0CLICCANDO\\\\xa0il link! http://forum.mxbars.net/viewtopic.php?f=18&t=50182SAN FRANCISCO. Oracle Park, CA. January 13, 2024. Ecco che la NUOVA stagione del Monster Energy Supercross 2024 continua, dopo Anaheim 1 si passa al secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non vede possibilità di chiudersi, per la lotta nella 450 dove sono tutti agguerriti e quest\\'anno il livello è ancora più alto ...\"}]')]}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "inputs = {\"question\": \"what is the weather in sf\", \"language\": \"italian\"}\n",
- "for s in app.stream(inputs):\n",
- " print(list(s.values())[0])\n",
- " print(\"----\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 25,
- "id": "fd60f5d6-bd4b-4995-80dd-63f268c17cff",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "AgentFinish(return_values={'output': 'Il clima a San Francisco durante il mese di gennaio è generalmente fresco con temperature medie di circa 9.6°C (49.2°F) e massime di 14°C (57.3°F). Si consiglia di prepararsi a temperature fresche se si pianifica una visita a San Francisco in gennaio.'}, log='Il clima a San Francisco durante il mese di gennaio è generalmente fresco con temperature medie di circa 9.6°C (49.2°F) e massime di 14°C (57.3°F). Si consiglia di prepararsi a temperature fresche se si pianifica una visita a San Francisco in gennaio.')"
- ]
- },
- "execution_count": 25,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "s[\"__end__\"][\"agent_outcome\"]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "20cac1a0-0c51-4cbd-ae27-929d71db2b56",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/examples/chat_agent_executor_with_function_calling/high-level.ipynb b/examples/chat_agent_executor_with_function_calling/high-level.ipynb
index a377367eb..55d985dda 100644
--- a/examples/chat_agent_executor_with_function_calling/high-level.ipynb
+++ b/examples/chat_agent_executor_with_function_calling/high-level.ipynb
@@ -5,111 +5,11 @@
"id": "8bcd1a3d-7c50-4f58-be4e-1ed654aa33be",
"metadata": {},
"source": [
- "# Chat Executor: with function calling\n",
+ "# (Deprecated) Chat Executor: with function calling\n",
"\n",
- "This notebook walks through an example creating a chat executor that uses function calling.\n",
- "This is useful for getting started quickly.\n",
- "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder."
+ "The function calling executor is deprecated in favor of [create_tool_calling_executor](../chat_agent_executor_with_function_calling/high-level-tools.ipynb).\n",
+ "This was done to better align with the underlying model providers' migration from \"function calling\" to \"tool calling\", which typically supports parallel tool usage."
]
- },
- {
- "cell_type": "markdown",
- "id": "e130cf70-a30e-47d7-8fd5-464f1a92e374",
- "metadata": {},
- "source": [
- "## Set up the chat model and tools\n",
- "\n",
- "Here we will define the chat model and tools that we want to use.\n",
- "Importantly, this model MUST support OpenAI function calling."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "id": "efb7e3c0-c63f-40f6-93ce-19681d650fc2",
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_openai import ChatOpenAI\n",
- "from langchain_community.tools.tavily_search import TavilySearchResults\n",
- "from langgraph.prebuilt import chat_agent_executor\n",
- "from langchain_core.messages import HumanMessage"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "a7025f33-3160-41cf-868b-17ebc916fb1d",
- "metadata": {},
- "outputs": [],
- "source": [
- "tools = [TavilySearchResults(max_results=1)]\n",
- "model = ChatOpenAI()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "43064805-2ac9-4b5a-850c-a68dd7282350",
- "metadata": {},
- "source": [
- "## Create executor\n",
- "\n",
- "We can now use the high level interface to create the executor"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "32b4ae66-f667-4a8b-a602-503fd0effcd9",
- "metadata": {},
- "outputs": [],
- "source": [
- "app = chat_agent_executor.create_function_calling_executor(model, tools)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "d63dbfc7-a5c1-4a03-991c-f0789ba52c52",
- "metadata": {},
- "source": [
- "We can now invoke this executor. The input to this must be a dictionary with a single `messsages` key that contains a list of messages."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "0abc5655-d772-450c-832f-1fee1111a5f6",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]}\n",
- "----\n",
- "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629', 'content': 'Get the current and future weather conditions for San Francisco, CA, including temperature, precipitation, wind, air quality and more. See the hourly and 10-day outlook, radar maps, alerts and allergy information.'}]\", name='tavily_search_results_json')]}\n",
- "----\n",
- "{'messages': [AIMessage(content='You can check the current and future weather conditions for San Francisco, CA on [AccuWeather](https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629).')]}\n",
- "----\n",
- "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629', 'content': 'Get the current and future weather conditions for San Francisco, CA, including temperature, precipitation, wind, air quality and more. See the hourly and 10-day outlook, radar maps, alerts and allergy information.'}]\", name='tavily_search_results_json'), AIMessage(content='You can check the current and future weather conditions for San Francisco, CA on [AccuWeather](https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629).')]}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
- "for s in app.stream(inputs):\n",
- " print(list(s.values())[0])\n",
- " print(\"----\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
@@ -128,7 +28,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.1"
+ "version": "3.11.2"
}
},
"nbformat": 4,
diff --git a/examples/visualization.ipynb b/examples/visualization.ipynb
index 86cd47340..234cbbc57 100644
--- a/examples/visualization.ipynb
+++ b/examples/visualization.ipynb
@@ -27,8 +27,8 @@
"id": "efb7e3c0-c63f-40f6-93ce-19681d650fc2",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:28.531482Z",
- "end_time": "2024-04-19T11:25:30.217991Z"
+ "end_time": "2024-04-19T11:25:30.217991Z",
+ "start_time": "2024-04-19T11:25:28.531482Z"
}
},
"outputs": [],
@@ -44,8 +44,8 @@
"id": "a7025f33-3160-41cf-868b-17ebc916fb1d",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:32.168821Z",
- "end_time": "2024-04-19T11:25:32.431922Z"
+ "end_time": "2024-04-19T11:25:32.431922Z",
+ "start_time": "2024-04-19T11:25:32.168821Z"
}
},
"outputs": [],
@@ -64,17 +64,15 @@
"id": "43064805-2ac9-4b5a-850c-a68dd7282350",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.469100Z",
- "end_time": "2024-04-18T12:18:30.586216Z"
+ "end_time": "2024-04-18T12:18:30.586216Z",
+ "start_time": "2024-04-18T12:18:30.469100Z"
}
},
"source": [
"## Create executor\n",
"\n",
"We can now use the high level interface to create the executor"
- ],
- "outputs": [],
- "execution_count": 3
+ ]
},
{
"cell_type": "code",
@@ -82,13 +80,13 @@
"id": "32b4ae66-f667-4a8b-a602-503fd0effcd9",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:36.098462Z",
- "end_time": "2024-04-19T11:25:36.231169Z"
+ "end_time": "2024-04-19T11:25:36.231169Z",
+ "start_time": "2024-04-19T11:25:36.098462Z"
}
},
"outputs": [],
"source": [
- "app = chat_agent_executor.create_function_calling_executor(model, tools)"
+ "app = chat_agent_executor.create_tool_calling_executor(model, tools)"
]
},
{
@@ -96,45 +94,15 @@
"id": "f4fc9378-b141-4b65-b86c-3afba77f7161",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.587191Z",
- "end_time": "2024-04-18T12:18:30.605220Z"
+ "end_time": "2024-04-18T12:18:30.605220Z",
+ "start_time": "2024-04-18T12:18:30.587191Z"
}
},
"source": [
"## Ascii\n",
"\n",
"We can easily visualize this graph in ascii"
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " +-----------+ \n",
- " | __start__ | \n",
- " +-----------+ \n",
- " * \n",
- " * \n",
- " * \n",
- " +-------+ \n",
- " | agent | \n",
- " +-------+* \n",
- " *** *** \n",
- " * * \n",
- " ** *** \n",
- "+-----------------+ * \n",
- "| should_continue | * \n",
- "+-----------------+. * \n",
- " . ..... * \n",
- " . ... * \n",
- " . ... * \n",
- " +---------+ +--------+ \n",
- " | __end__ | | action | \n",
- " +---------+ +--------+ \n"
- ]
- }
- ],
- "execution_count": 4
+ ]
},
{
"cell_type": "code",
@@ -142,8 +110,8 @@
"id": "ca9b980d-1f0a-4286-9157-a870e3d55134",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:37.273032Z",
- "end_time": "2024-04-19T11:25:37.303260Z"
+ "end_time": "2024-04-19T11:25:37.303260Z",
+ "start_time": "2024-04-19T11:25:37.273032Z"
}
},
"outputs": [
@@ -151,27 +119,21 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " +-----------+ \n",
- " | __start__ | \n",
- " +-----------+ \n",
- " * \n",
- " * \n",
- " * \n",
- " +-------+ \n",
- " | agent | \n",
- " +-------+* \n",
- " *** *** \n",
- " * * \n",
- " ** *** \n",
- "+-----------------+ * \n",
- "| should_continue | * \n",
- "+-----------------+. * \n",
- " . ..... * \n",
- " . ... * \n",
- " . ... * \n",
- " +---------+ +--------+ \n",
- " | __end__ | | action | \n",
- " +---------+ +--------+ \n"
+ " +-----------+ \n",
+ " | __start__ | \n",
+ " +-----------+ \n",
+ " * \n",
+ " * \n",
+ " * \n",
+ " +-------+ \n",
+ " | agent | \n",
+ " +-------+ \n",
+ " * .. \n",
+ " ** .. \n",
+ " * . \n",
+ "+--------+ +---------+ \n",
+ "| action | | __end__ | \n",
+ "+--------+ +---------+ \n"
]
}
],
@@ -184,40 +146,15 @@
"id": "edcd9ad2",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.609323Z",
- "end_time": "2024-04-18T12:18:30.629307Z"
+ "end_time": "2024-04-18T12:18:30.629307Z",
+ "start_time": "2024-04-18T12:18:30.609323Z"
}
},
"source": [
"## Mermaid\n",
"\n",
"We can also convert a graph class into Mermaid syntax."
- ],
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "%%{init: {'flowchart': {'curve': 'linear'}}}%%\n",
- "graph TD;\n",
- "\t__start__[__start__]:::startclass;\n",
- "\t__end__[__end__]:::endclass;\n",
- "\tagent([agent]):::otherclass;\n",
- "\taction([action]):::otherclass;\n",
- "\tshould_continue([should_continue]):::otherclass;\n",
- "\t__start__ --> agent;\n",
- "\taction --> agent;\n",
- "\tagent --> should_continue;\n",
- "\tshould_continue -. continue .-> action;\n",
- "\tshould_continue -. end .-> __end__;\n",
- "\tclassDef startclass fill:#ffdfba;\n",
- "\tclassDef endclass fill:#baffc9;\n",
- "\tclassDef otherclass fill:#fad7de;\n",
- "\n"
- ]
- }
- ],
- "execution_count": 5
+ ]
},
{
"cell_type": "code",
@@ -225,8 +162,8 @@
"id": "66007b2d",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:38.726838Z",
- "end_time": "2024-04-19T11:25:38.733126Z"
+ "end_time": "2024-04-19T11:25:38.733126Z",
+ "start_time": "2024-04-19T11:25:38.726838Z"
}
},
"outputs": [
@@ -240,12 +177,10 @@
"\t__end__[__end__]:::endclass;\n",
"\tagent([agent]):::otherclass;\n",
"\taction([action]):::otherclass;\n",
- "\tshould_continue([should_continue]):::otherclass;\n",
"\t__start__ --> agent;\n",
"\taction --> agent;\n",
- "\tagent --> should_continue;\n",
- "\tshould_continue -. continue .-> action;\n",
- "\tshould_continue -. end .-> __end__;\n",
+ "\tagent -. continue .-> action;\n",
+ "\tagent -. end .-> __end__;\n",
"\tclassDef startclass fill:#ffdfba;\n",
"\tclassDef endclass fill:#baffc9;\n",
"\tclassDef otherclass fill:#fad7de;\n",
@@ -262,8 +197,8 @@
"id": "324d40ed-b665-4416-88f1-5df161546cd9",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.615432Z",
- "end_time": "2024-04-18T12:18:30.629548Z"
+ "end_time": "2024-04-18T12:18:30.629548Z",
+ "start_time": "2024-04-18T12:18:30.615432Z"
}
},
"source": [
@@ -274,13 +209,19 @@
"- Using graphviz (which requires `pip install graphviz`)\n",
"- Using Mermaid + Pyppeteer (requires `pip install pyppeteer`)\n",
"- Using Mermaid.ink API (does not require additional packages)"
- ],
- "outputs": [],
- "execution_count": 6
+ ]
},
{
"cell_type": "code",
"execution_count": 6,
+ "id": "df39af17",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-04-19T11:25:40.358604Z",
+ "start_time": "2024-04-19T11:25:40.351636Z"
+ },
+ "collapsed": false
+ },
"outputs": [],
"source": [
"from IPython.display import display, HTML\n",
@@ -290,29 +231,20 @@
" decoded_img_bytes = base64.b64encode(image_bytes).decode('utf-8')\n",
" html = f'
'\n",
" display(HTML(html))"
- ],
- "metadata": {
- "collapsed": false,
- "ExecuteTime": {
- "start_time": "2024-04-19T11:25:40.351636Z",
- "end_time": "2024-04-19T11:25:40.358604Z"
- }
- }
+ ]
},
{
"cell_type": "markdown",
"id": "d821b2f6",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.620092Z",
- "end_time": "2024-04-18T12:18:30.629629Z"
+ "end_time": "2024-04-18T12:18:30.629629Z",
+ "start_time": "2024-04-18T12:18:30.620092Z"
}
},
"source": [
"### Using Graphviz"
- ],
- "outputs": [],
- "execution_count": 7
+ ]
},
{
"cell_type": "code",
@@ -320,11 +252,24 @@
"id": "d4234400-75cd-4b13-aeff-828f7fb68ab1",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:42.019017Z",
- "end_time": "2024-04-19T11:25:42.057704Z"
+ "end_time": "2024-04-19T11:25:42.057704Z",
+ "start_time": "2024-04-19T11:25:42.019017Z"
}
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: install in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (1.3.5)\n",
+ "Collecting pygraphviz\n",
+ " Using cached pygraphviz-1.12-cp311-cp311-macosx_13_0_arm64.whl\n",
+ "Installing collected packages: pygraphviz\n",
+ "Successfully installed pygraphviz-1.12\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
"source": [
"#%%capture --no-stderr\n",
"%pip install pygraphviz"
@@ -336,15 +281,19 @@
"id": "ee026342-f560-4ce0-ab43-1718bd19a366",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:42.452377Z",
- "end_time": "2024-04-19T11:25:42.631675Z"
+ "end_time": "2024-04-19T11:25:42.631675Z",
+ "start_time": "2024-04-19T11:25:42.452377Z"
}
},
"outputs": [
{
"data": {
- "text/plain": "",
- "text/html": "
"
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ ""
+ ]
},
"metadata": {},
"output_type": "display_data"
@@ -359,15 +308,13 @@
"id": "b9e767fc",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:18:30.871750Z",
- "end_time": "2024-04-18T12:18:30.873950Z"
+ "end_time": "2024-04-18T12:18:30.873950Z",
+ "start_time": "2024-04-18T12:18:30.871750Z"
}
},
"source": [
"### Using Mermaid + Pyppeteer"
- ],
- "outputs": [],
- "execution_count": 9
+ ]
},
{
"cell_type": "code",
@@ -375,11 +322,57 @@
"id": "d403e1e7",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:44.793438Z",
- "end_time": "2024-04-19T11:25:44.798703Z"
+ "end_time": "2024-04-19T11:25:44.798703Z",
+ "start_time": "2024-04-19T11:25:44.793438Z"
}
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: install in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (1.3.5)\n",
+ "Collecting pyppeteer\n",
+ " Downloading pyppeteer-2.0.0-py3-none-any.whl.metadata (7.1 kB)\n",
+ "Collecting appdirs<2.0.0,>=1.4.3 (from pyppeteer)\n",
+ " Downloading appdirs-1.4.4-py2.py3-none-any.whl.metadata (9.0 kB)\n",
+ "Requirement already satisfied: certifi>=2023 in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (from pyppeteer) (2024.2.2)\n",
+ "Requirement already satisfied: importlib-metadata>=1.4 in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (from pyppeteer) (6.11.0)\n",
+ "Collecting pyee<12.0.0,>=11.0.0 (from pyppeteer)\n",
+ " Downloading pyee-11.1.0-py3-none-any.whl.metadata (2.8 kB)\n",
+ "Requirement already satisfied: tqdm<5.0.0,>=4.42.1 in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (from pyppeteer) (4.66.2)\n",
+ "Collecting urllib3<2.0.0,>=1.25.8 (from pyppeteer)\n",
+ " Using cached urllib3-1.26.18-py2.py3-none-any.whl.metadata (48 kB)\n",
+ "Collecting websockets<11.0,>=10.0 (from pyppeteer)\n",
+ " Downloading websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl.metadata (6.4 kB)\n",
+ "Requirement already satisfied: zipp>=0.5 in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (from importlib-metadata>=1.4->pyppeteer) (3.17.0)\n",
+ "Requirement already satisfied: typing-extensions in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (from pyee<12.0.0,>=11.0.0->pyppeteer) (4.10.0)\n",
+ "Downloading pyppeteer-2.0.0-py3-none-any.whl (82 kB)\n",
+ "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m82.9/82.9 kB\u001b[0m \u001b[31m2.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+ "\u001b[?25hDownloading appdirs-1.4.4-py2.py3-none-any.whl (9.6 kB)\n",
+ "Downloading pyee-11.1.0-py3-none-any.whl (15 kB)\n",
+ "Using cached urllib3-1.26.18-py2.py3-none-any.whl (143 kB)\n",
+ "Downloading websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl (97 kB)\n",
+ "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m97.9/97.9 kB\u001b[0m \u001b[31m6.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+ "\u001b[?25hInstalling collected packages: appdirs, websockets, urllib3, pyee, pyppeteer\n",
+ " Attempting uninstall: websockets\n",
+ " Found existing installation: websockets 12.0\n",
+ " Uninstalling websockets-12.0:\n",
+ " Successfully uninstalled websockets-12.0\n",
+ " Attempting uninstall: urllib3\n",
+ " Found existing installation: urllib3 2.2.1\n",
+ " Uninstalling urllib3-2.2.1:\n",
+ " Successfully uninstalled urllib3-2.2.1\n",
+ "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n",
+ "types-requests 2.31.0.20240311 requires urllib3>=2, but you have urllib3 1.26.18 which is incompatible.\u001b[0m\u001b[31m\n",
+ "\u001b[0mSuccessfully installed appdirs-1.4.4 pyee-11.1.0 pyppeteer-2.0.0 urllib3-1.26.18 websockets-10.4\n",
+ "Note: you may need to restart the kernel to use updated packages.\n",
+ "Requirement already satisfied: install in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (1.3.5)\n",
+ "Requirement already satisfied: nest_asyncio in /Users/wfh/code/lc/langgraph/.venv/lib/python3.11/site-packages (1.6.0)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
"source": [
"# %%capture --no-stderr\n",
"%pip install pyppeteer\n",
@@ -393,15 +386,29 @@
"id": "058546ee",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:45.405158Z",
- "end_time": "2024-04-19T11:25:47.412695Z"
+ "end_time": "2024-04-19T11:25:47.412695Z",
+ "start_time": "2024-04-19T11:25:45.405158Z"
}
},
"outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[INFO] Starting Chromium download.\n",
+ "100%|██████████| 141M/141M [00:09<00:00, 14.2Mb/s] \n",
+ "[INFO] Beginning extraction\n",
+ "[INFO] Chromium extracted to: /Users/wfh/Library/Application Support/pyppeteer/local-chromium/1181205\n"
+ ]
+ },
{
"data": {
- "text/plain": "",
- "text/html": "
"
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ ""
+ ]
},
"metadata": {},
"output_type": "display_data"
@@ -429,24 +436,13 @@
"id": "2dd71a7c",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-18T12:16:57.852988Z",
- "end_time": "2024-04-18T12:16:58.610115Z"
+ "end_time": "2024-04-18T12:16:58.610115Z",
+ "start_time": "2024-04-18T12:16:57.852988Z"
}
},
"source": [
"### Using Mermaid.Ink"
- ],
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "text/html": "
"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "execution_count": 12
+ ]
},
{
"cell_type": "code",
@@ -454,15 +450,19 @@
"id": "be37d419",
"metadata": {
"ExecuteTime": {
- "start_time": "2024-04-19T11:25:51.640462Z",
- "end_time": "2024-04-19T11:25:51.865932Z"
+ "end_time": "2024-04-19T11:25:51.865932Z",
+ "start_time": "2024-04-19T11:25:51.640462Z"
}
},
"outputs": [
{
"data": {
- "text/plain": "",
- "text/html": "
"
+ "text/html": [
+ "
"
+ ],
+ "text/plain": [
+ ""
+ ]
},
"metadata": {},
"output_type": "display_data"
@@ -473,57 +473,6 @@
" draw_method=MermaidDrawMethod.API,\n",
"))"
]
- },
- {
- "cell_type": "markdown",
- "id": "e3079261",
- "metadata": {
- "ExecuteTime": {
- "start_time": "2024-04-18T12:18:34.010816Z",
- "end_time": "2024-04-18T12:18:35.651423Z"
- }
- },
- "source": [
- "## Excluding condition nodes\n",
- "By default, condition nods like 'should_continue' will be added. In case you have a big graph and want to exclude these or simplicity, you can use add_condition_nodes parameter"
- ],
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "text/html": "
"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "execution_count": 13
- },
- {
- "cell_type": "code",
- "execution_count": 21,
- "id": "9f2773dd",
- "metadata": {
- "ExecuteTime": {
- "start_time": "2024-04-19T17:28:35.404649Z",
- "end_time": "2024-04-19T17:28:37.844424Z"
- }
- },
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "text/html": "
"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "display_image(app.get_graph(add_condition_nodes=False).draw_mermaid_png(\n",
- " draw_method=MermaidDrawMethod.PYPPETEER,\n",
- "))\n"
- ]
}
],
"metadata": {
@@ -542,7 +491,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.1"
+ "version": "3.11.2"
}
},
"nbformat": 4,
diff --git a/langgraph/_api/__init__.py b/langgraph/_api/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/langgraph/_api/deprecation.py b/langgraph/_api/deprecation.py
new file mode 100644
index 000000000..b8e310464
--- /dev/null
+++ b/langgraph/_api/deprecation.py
@@ -0,0 +1,34 @@
+import functools
+import warnings
+from typing import Callable, TypeVar
+
+
+class LangGraphDeprecationWarning(DeprecationWarning):
+ pass
+
+
+F = TypeVar("F", bound=Callable)
+
+
+def deprecated(version: str, alternative: str, *, example: str = ""):
+ def decorator(func: F) -> F:
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ message = (
+ f"{func.__name__} is deprecated as of version {version} and will be"
+ f" removed in a future version. Use {alternative} instead.{example}"
+ )
+ warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2)
+ return func(*args, **kwargs)
+
+ docstring = (
+ f"**Deprecated**: This function is deprecated as of version {version}. "
+ f"Use `{alternative}` instead."
+ )
+ if func.__doc__:
+ docstring = docstring + f"\n\n{func.__doc__}"
+ wrapper.__doc__ = docstring
+
+ return wrapper
+
+ return decorator
diff --git a/langgraph/checkpoint/aiosqlite.py b/langgraph/checkpoint/aiosqlite.py
index f033953af..62818dc16 100644
--- a/langgraph/checkpoint/aiosqlite.py
+++ b/langgraph/checkpoint/aiosqlite.py
@@ -18,12 +18,61 @@ from langgraph.checkpoint.sqlite import JsonPlusSerializerCompat
class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
+ """An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
+
+ Note: Requires the `aiosqlite` package. Install it with `pip install aiosqlite`.
+
+ Args:
+ conn (aiosqlite.Connection): The asynchronous SQLite database connection.
+ serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to JsonPlusSerializerCompat.
+ at (Optional[CheckpointAt]): The checkpoint strategy to use. Defaults to None.
+
+ Examples:
+
+ Usage within a StateGraph:
+
+ import asyncio
+ import aiosqlite
+
+ from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver
+ from langgraph.graph import StateGraph
+
+ builder = StateGraph(int)
+ builder.add_node("add_one", lambda x: x + 1)
+ builder.set_entry_point("add_one")
+ builder.set_finish_point("add_one")
+
+ memory = AsyncSqliteSaver.from_conn_string("checkpoints.sqlite")
+ graph = builder.compile(checkpointer=memory)
+ coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
+ asyncio.run(coro) # Output: 2
+
+
+ Raw usage:
+
+ import asyncio
+ import aiosqlite
+ from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver
+
+
+ async def main():
+ async with aiosqlite.connect("checkpoints.db") as conn:
+ saver = AsyncSqliteSaver(conn)
+ config = {"configurable": {"thread_id": "1"}}
+ checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}}
+ saved_config = await saver.aput(config, checkpoint)
+ print(
+ saved_config
+ ) # Output: {"configurable": {"thread_id": "1", "thread_ts": "2023-05-03T10:00:00Z"}}
+
+
+ asyncio.run(main())
+ """
+
serde = JsonPlusSerializerCompat()
conn: aiosqlite.Connection
-
lock: asyncio.Lock
-
is_setup: bool
def __init__(
@@ -40,6 +89,14 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
@classmethod
def from_conn_string(cls, conn_string: str) -> "AsyncSqliteSaver":
+ """Create a new AsyncSqliteSaver instance from a connection string.
+
+ Args:
+ conn_string (str): The SQLite connection string.
+
+ Returns:
+ AsyncSqliteSaver: A new AsyncSqliteSaver instance.
+ """
return AsyncSqliteSaver(conn=aiosqlite.connect(conn_string))
async def __aenter__(self) -> Self:
@@ -55,6 +112,12 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
return await self.conn.close()
async def setup(self) -> None:
+ """Set up the checkpoint database asynchronously.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist. It is called automatically when needed and should not be called
+ directly by the user.
+ """
async with self.lock:
if self.is_setup:
return
@@ -76,6 +139,19 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
self.is_setup = True
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
+ """Get a checkpoint tuple from the database asynchronously.
+
+ This method retrieves a checkpoint tuple from the SQLite database based on the
+ provided config. If the config contains a "thread_ts" key, the checkpoint with
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
+
+ Returns:
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
await self.setup()
if config["configurable"].get("thread_ts"):
async with self.conn.execute(
@@ -89,14 +165,16 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
return CheckpointTuple(
config,
self.serde.loads(value[0]),
- {
- "configurable": {
- "thread_id": config["configurable"]["thread_id"],
- "thread_ts": value[1],
+ (
+ {
+ "configurable": {
+ "thread_id": config["configurable"]["thread_id"],
+ "thread_ts": value[1],
+ }
}
- }
- if value[1]
- else None,
+ if value[1]
+ else None
+ ),
)
else:
async with self.conn.execute(
@@ -112,14 +190,16 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
}
},
self.serde.loads(value[3]),
- {
- "configurable": {
- "thread_id": value[0],
- "thread_ts": value[2],
+ (
+ {
+ "configurable": {
+ "thread_id": value[0],
+ "thread_ts": value[2],
+ }
}
- }
- if value[2]
- else None,
+ if value[2]
+ else None
+ ),
)
async def alist(
@@ -129,6 +209,19 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> AsyncIterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ This method retrieves a list of checkpoint tuples from the SQLite database based
+ on the provided config. The checkpoints are ordered by timestamp in descending order.
+
+ Args:
+ config (RunnableConfig): The config to use for listing the checkpoints.
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.
+ limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
+
+ Yields:
+ AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
+ """
await self.setup()
query = (
"SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC"
@@ -139,25 +232,46 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager):
query += f" LIMIT {limit}"
async with self.conn.execute(
query,
- (str(config["configurable"]["thread_id"]),)
- if before is None
- else (
- str(config["configurable"]["thread_id"]),
- str(before["configurable"]["thread_ts"]),
+ (
+ (str(config["configurable"]["thread_id"]),)
+ if before is None
+ else (
+ str(config["configurable"]["thread_id"]),
+ str(before["configurable"]["thread_ts"]),
+ )
),
) as cursor:
async for thread_id, thread_ts, parent_ts, value in cursor:
yield CheckpointTuple(
{"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}},
self.serde.loads(value),
- {"configurable": {"thread_id": thread_id, "thread_ts": parent_ts}}
- if parent_ts
- else None,
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "thread_ts": parent_ts,
+ }
+ }
+ if parent_ts
+ else None
+ ),
)
async def aput(
self, config: RunnableConfig, checkpoint: Checkpoint
) -> RunnableConfig:
+ """Save a checkpoint to the database asynchronously.
+
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config (RunnableConfig): The config to associate with the checkpoint.
+ checkpoint (Checkpoint): The checkpoint to save.
+
+ Returns:
+ RunnableConfig: The updated config containing the saved checkpoint's timestamp.
+ """
await self.setup()
async with self.conn.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, thread_ts, parent_ts, checkpoint) VALUES (?, ?, ?, ?)",
diff --git a/langgraph/checkpoint/memory.py b/langgraph/checkpoint/memory.py
index dad5e8dc8..055f92d54 100644
--- a/langgraph/checkpoint/memory.py
+++ b/langgraph/checkpoint/memory.py
@@ -14,6 +14,31 @@ from langgraph.checkpoint.base import (
class MemorySaver(BaseCheckpointSaver):
+ """An in-memory checkpoint saver.
+
+ This checkpoint saver stores checkpoints in memory using a defaultdict.
+
+ Args:
+ serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to None.
+ at (Optional[CheckpointAt]): The checkpoint strategy to use. Defaults to None.
+
+ Examples:
+ import asyncio
+
+ from langgraph.checkpoint.memory import MemorySaver
+ from langgraph.graph import StateGraph
+
+ builder = StateGraph(int)
+ builder.add_node("add_one", lambda x: x + 1)
+ builder.set_entry_point("add_one")
+ builder.set_finish_point("add_one")
+
+ memory = MemorySaver()
+ graph = builder.compile(checkpointer=memory)
+ coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
+ asyncio.run(coro) # Output: 2
+ """
+
storage: defaultdict[str, dict[str, Checkpoint]]
def __init__(
@@ -26,6 +51,19 @@ class MemorySaver(BaseCheckpointSaver):
self.storage = defaultdict(dict)
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
+ """Get a checkpoint tuple from the in-memory storage.
+
+ This method retrieves a checkpoint tuple from the in-memory storage based on the
+ provided config. If the config contains a "thread_ts" key, the checkpoint with
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
+
+ Returns:
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
thread_id = config["configurable"]["thread_id"]
if ts := config["configurable"].get("thread_ts"):
if checkpoint := self.storage[thread_id].get(ts):
@@ -47,6 +85,19 @@ class MemorySaver(BaseCheckpointSaver):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the in-memory storage.
+
+ This method retrieves a list of checkpoint tuples from the in-memory storage based
+ on the provided config. The checkpoints are ordered by timestamp in descending order.
+
+ Args:
+ config (RunnableConfig): The config to use for listing the checkpoints.
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.
+ limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
+
+ Yields:
+ Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
+ """
thread_id = config["configurable"]["thread_id"]
for ts, checkpoint in self.storage[thread_id].items():
if before and ts >= before["configurable"]["thread_ts"]:
@@ -60,6 +111,18 @@ class MemorySaver(BaseCheckpointSaver):
)
def put(self, config: RunnableConfig, checkpoint: Checkpoint) -> RunnableConfig:
+ """Save a checkpoint to the in-memory storage.
+
+ This method saves a checkpoint to the in-memory storage. The checkpoint is associated
+ with the provided config.
+
+ Args:
+ config (RunnableConfig): The config to associate with the checkpoint.
+ checkpoint (Checkpoint): The checkpoint to save.
+
+ Returns:
+ RunnableConfig: The updated config containing the saved checkpoint's timestamp.
+ """
self.storage[config["configurable"]["thread_id"]].update(
{checkpoint["ts"]: self.serde.dumps(checkpoint)}
)
@@ -71,11 +134,33 @@ class MemorySaver(BaseCheckpointSaver):
}
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
+ """Asynchronous version of get_tuple.
+
+ This method is an asynchronous wrapper around get_tuple that runs the synchronous
+ method in a separate thread using asyncio.
+
+ Args:
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
+
+ Returns:
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
return await asyncio.get_running_loop().run_in_executor(
None, self.get_tuple, config
)
async def alist(self, config: RunnableConfig) -> AsyncIterator[CheckpointTuple]:
+ """Asynchronous version of list.
+
+ This method is an asynchronous wrapper around list that runs the synchronous
+ method in a separate thread using asyncio.
+
+ Args:
+ config (RunnableConfig): The config to use for listing the checkpoints.
+
+ Yields:
+ AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
+ """
loop = asyncio.get_running_loop()
iter = loop.run_in_executor(None, self.list, config)
while True:
diff --git a/langgraph/checkpoint/sqlite.py b/langgraph/checkpoint/sqlite.py
index 7d2344b45..c685a5842 100644
--- a/langgraph/checkpoint/sqlite.py
+++ b/langgraph/checkpoint/sqlite.py
@@ -17,8 +17,30 @@ from langgraph.checkpoint.base import (
from langgraph.serde.jsonplus import JsonPlusSerializer
-# for backwards compat we continue to support loading pickled checkpoints
class JsonPlusSerializerCompat(JsonPlusSerializer):
+ """A serializer that supports loading pickled checkpoints for backwards compatibility.
+
+ This serializer extends the JsonPlusSerializer and adds support for loading pickled
+ checkpoints. If the input data starts with b"\x80" and ends with b".", it is treated
+ as a pickled checkpoint and loaded using pickle.loads(). Otherwise, the default
+ JsonPlusSerializer behavior is used.
+
+ Examples:
+
+ import pickle
+
+ from langgraph.checkpoint.sqlite import JsonPlusSerializerCompat
+
+ serializer = JsonPlusSerializerCompat()
+ pickled_data = pickle.dumps({"key": "value"})
+ loaded_data = serializer.loads(pickled_data)
+ print(loaded_data) # Output: {"key": "value"}
+
+ json_data = '{"key": "value"}'.encode("utf-8")
+ loaded_data = serializer.loads(json_data)
+ print(loaded_data) # Output: {"key": "value"}
+ """
+
def loads(self, data: bytes) -> Any:
if data.startswith(b"\x80") and data.endswith(b"."):
return pickle.loads(data)
@@ -26,10 +48,41 @@ class JsonPlusSerializerCompat(JsonPlusSerializer):
class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
+ """A checkpoint saver that stores checkpoints in a SQLite database.
+
+ Note: While useful for demos and small projects, this class does not
+ scale to multiple threads.
+
+ Args:
+ conn (sqlite3.Connection): The SQLite database connection.
+ serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to JsonPlusSerializerCompat.
+ at (Optional[CheckpointAt]): The checkpoint strategy to use. Defaults to None.
+
+ Examples:
+
+ import sqlite3
+
+ from langgraph.checkpoint.sqlite import SqliteSaver
+ from langgraph.graph import StateGraph
+
+ builder = StateGraph(int)
+ builder.add_node("add_one", lambda x: x + 1)
+ builder.set_entry_point("add_one")
+ builder.set_finish_point("add_one")
+ conn = sqlite3.connect("checkpoints.sqlite")
+ memory = SqliteSaver(conn)
+ graph = builder.compile(checkpointer=memory)
+
+ config = {"configurable": {"thread_id": "1"}}
+ # checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}}
+ result = graph.invoke(3, config)
+ graph.get_state(config)
+ # Output: StateSnapshot(values=4, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '2024-05-04T06:32:42.235444+00:00'}}, parent_config=None)
+ """ # noqa
+
serde = JsonPlusSerializerCompat()
conn: sqlite3.Connection
-
is_setup: bool
def __init__(
@@ -45,6 +98,24 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
@classmethod
def from_conn_string(cls, conn_string: str) -> "SqliteSaver":
+ """Create a new SqliteSaver instance from a connection string.
+
+ Args:
+ conn_string (str): The SQLite connection string.
+
+ Returns:
+ SqliteSaver: A new SqliteSaver instance.
+
+ Examples:
+
+ In memory:
+
+ memory = SqliteSaver.from_conn_string(":memory:")
+
+ To disk:
+
+ memory = SqliteSaver.from_conn_string("checkpoints.sqlite")
+ """
return SqliteSaver(conn=sqlite3.connect(conn_string))
def __enter__(self) -> Self:
@@ -59,6 +130,12 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
return self.conn.close()
def setup(self) -> None:
+ """Set up the checkpoint database.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist. It is called automatically when needed and should not be called
+ directly by the user.
+ """
if self.is_setup:
return
@@ -78,6 +155,17 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
@contextmanager
def cursor(self, transaction: bool = True):
+ """Get a cursor for the SQLite database.
+
+ This method returns a cursor for the SQLite database. It is used internally
+ by the SqliteSaver and should not be called directly by the user.
+
+ Args:
+ transaction (bool): Whether to commit the transaction when the cursor is closed. Defaults to True.
+
+ Yields:
+ sqlite3.Cursor: A cursor for the SQLite database.
+ """
self.setup()
cur = self.conn.cursor()
try:
@@ -88,6 +176,38 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
cur.close()
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the SQLite database based on the
+ provided config. If the config contains a "thread_ts" key, the checkpoint with
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config (RunnableConfig): The config to use for retrieving the checkpoint.
+
+ Returns:
+ Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+
+ Examples:
+
+ Basic:
+
+ config = {"configurable": {"thread_id": "1"}}
+ checkpoint_tuple = memory.get_tuple(config)
+ print(checkpoint_tuple) # Output: CheckpointTuple(...)
+
+ With timestamp:
+
+ config = {
+ "configurable": {
+ "thread_id": "1",
+ "thread_ts": "2024-05-04T06:32:42.235444+00:00",
+ }
+ }
+ checkpoint_tuple = memory.get_tuple(config)
+ print(checkpoint_tuple) # Output: CheckpointTuple(...)
+ """ # noqa
with self.cursor(transaction=False) as cur:
if config["configurable"].get("thread_ts"):
cur.execute(
@@ -101,14 +221,16 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
return CheckpointTuple(
config,
self.serde.loads(value[0]),
- {
- "configurable": {
- "thread_id": config["configurable"]["thread_id"],
- "thread_ts": value[1],
+ (
+ {
+ "configurable": {
+ "thread_id": config["configurable"]["thread_id"],
+ "thread_ts": value[1],
+ }
}
- }
- if value[1]
- else None,
+ if value[1]
+ else None
+ ),
)
else:
cur.execute(
@@ -124,14 +246,16 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
}
},
self.serde.loads(value[3]),
- {
- "configurable": {
- "thread_id": value[0],
- "thread_ts": value[2],
+ (
+ {
+ "configurable": {
+ "thread_id": value[0],
+ "thread_ts": value[2],
+ }
}
- }
- if value[2]
- else None,
+ if value[2]
+ else None
+ ),
)
def list(
@@ -141,6 +265,29 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the SQLite database based
+ on the provided config. The checkpoints are ordered by timestamp in descending order.
+
+ Args:
+ config (RunnableConfig): The config to use for listing the checkpoints.
+ before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.
+ limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
+
+ Yields:
+ Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
+
+ Examples:
+ config = {"configurable": {"thread_id": "1"}}
+ checkpoints = list(memory.list(config, limit=2))
+ print(checkpoints) # Output: [CheckpointTuple(...), CheckpointTuple(...)]
+
+ config = {"configurable": {"thread_id": "1"}}
+ before = {"configurable": {"thread_ts": "2024-05-04T06:32:42.235444+00:00"}}
+ checkpoints = list(memory.list(config, before=before))
+ print(checkpoints) # Output: [CheckpointTuple(...), ...]
+ """
query = (
"SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC"
if before is None
@@ -151,28 +298,53 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager):
with self.cursor(transaction=False) as cur:
cur.execute(
query,
- (str(config["configurable"]["thread_id"]),)
- if before is None
- else (
- str(config["configurable"]["thread_id"]),
- before["configurable"]["thread_ts"],
+ (
+ (str(config["configurable"]["thread_id"]),)
+ if before is None
+ else (
+ str(config["configurable"]["thread_id"]),
+ before["configurable"]["thread_ts"],
+ )
),
)
for thread_id, thread_ts, parent_ts, value in cur:
yield CheckpointTuple(
{"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}},
self.serde.loads(value),
- {
- "configurable": {
- "thread_id": thread_id,
- "thread_ts": parent_ts,
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "thread_ts": parent_ts,
+ }
}
- }
- if parent_ts
- else None,
+ if parent_ts
+ else None
+ ),
)
def put(self, config: RunnableConfig, checkpoint: Checkpoint) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config (RunnableConfig): The config to associate with the checkpoint.
+ checkpoint (Checkpoint): The checkpoint to save.
+
+ Returns:
+ RunnableConfig: The updated config containing the saved checkpoint's timestamp.
+
+ Examples:
+
+ config = {"configurable": {"thread_id": "1"}}
+ checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "data": {"key": "value"}}
+ saved_config = memory.put(config, checkpoint)
+ print(
+ saved_config
+ ) # Output: {"configurable": {"thread_id": "1", "thread_ts": 2024-05-04T06:32:42.235444+00:00"}}
+ """
with self.cursor() as cur:
cur.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, thread_ts, parent_ts, checkpoint) VALUES (?, ?, ?, ?)",
diff --git a/langgraph/graph/message.py b/langgraph/graph/message.py
index fa2b29ff1..62fc39f2c 100644
--- a/langgraph/graph/message.py
+++ b/langgraph/graph/message.py
@@ -14,6 +14,53 @@ Messages = Union[list[MessageLikeRepresentation], MessageLikeRepresentation]
def add_messages(left: Messages, right: Messages) -> Messages:
+ """Merges two lists of messages, updating existing messages by ID.
+
+ By default, this ensures the state is "append-only", unless the
+ new message has the same ID as an existing message.
+
+ Args:
+ left: The base list of messages.
+ right: The list of messages (or single message) to merge
+ into the base list.
+
+ Returns:
+ A new list of messages with the messages from `right` merged into `left`.
+ If a message in `right` has the same ID as a message in `left`, the
+ message from `right` will replace the message from `left`.
+
+ Examples:
+
+ msgs1 = [HumanMessage(content="Hello", id="1")]
+ msgs2 = [AIMessage(content="Hi there!", id="2")]
+ add_messages(msgs1, msgs2)
+ # [HumanMessage(content="Hello", id="1"), AIMessage(content="Hi there!", id="2")]
+
+
+ msgs1 = [HumanMessage(content="Hello", id="1")]
+ msgs2 = [HumanMessage(content="Hello again", id="1")]
+ add_messages(msgs1, msgs2)
+ # [HumanMessage(content="Hello again", id="1")]
+
+
+ from typing import Annotated
+ from typing_extensions import TypedDict
+ from langgraph.graph import StateGraph
+
+
+ class State(TypedDict):
+ messages: Annotated[list, add_messages]
+
+
+ builder = StateGraph(State)
+ builder.add_node("chatbot", lambda state: {"messages": [("assistant", "Hello")]})
+ builder.set_entry_point("chatbot")
+ builder.set_finish_point("chatbot")
+ graph = builder.compile()
+ graph.invoke({})
+ # {'messages': [AIMessage(content='Hello', id='f657fb65-b6af-4790-a5b5-1d266a2ed26e')]}
+
+ """
# coerce to list
if not isinstance(left, list):
left = [left]
@@ -41,9 +88,51 @@ def add_messages(left: Messages, right: Messages) -> Messages:
class MessageGraph(StateGraph):
- """A StateGraph where every node
- - receives a list of messages as input
- - returns one or more messages as output."""
+ """A StateGraph where every node receives a list of messages as input and returns one or more messages as output.
+
+ MessageGraph is a subclass of StateGraph whose entire state is a single, append-only* list of messages.
+ Each node in a MessageGraph takes a list of messages as input and returns zero or more
+ messages as output. The `add_messages` function is used to merge the output messages from each node
+ into the existing list of messages in the graph's state.
+
+ Examples:
+
+ from langgraph.graph.message import MessageGraph
+
+ builder = MessageGraph()
+ builder.add_node("chatbot", lambda state: [("assistant", "Hello!")])
+ builder.set_entry_point("chatbot")
+ builder.set_finish_point("chatbot")
+ builder.compile().invoke([("user", "Hi there.")])
+ # {'messages': [HumanMessage(content="Hi there.", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'),
+ # AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8')]}
+
+
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
+
+ from langgraph.graph.message import MessageGraph
+
+ builder = MessageGraph()
+ builder.add_node(
+ "chatbot",
+ lambda state: [
+ AIMessage(
+ content="Hello!",
+ tool_calls=[{"name": "search", "id": "123", "args": {"query": "X"}}],
+ )
+ ],
+ )
+ builder.add_node(
+ "search", lambda state: [ToolMessage(content="Searching...", tool_call_id="123")]
+ )
+ builder.set_entry_point("chatbot")
+ builder.add_edge("chatbot", "search")
+ builder.set_finish_point("search")
+ builder.compile().invoke([HumanMessage(content="Hi there. Can you search for X?")])
+ # {'messages': [HumanMessage(content="Hi there. Can you search for X?", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'),
+ # AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8'),
+ # ToolMessage(content="Searching...", id='d8f4f4d9-c1d8-4f4d-b8b7-d8f4f4d9c1d8', tool_call_id="123")]}
+ """
def __init__(self) -> None:
super().__init__(Annotated[list[AnyMessage], add_messages])
diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py
index cb240b1e2..c2e6e1a00 100644
--- a/langgraph/graph/state.py
+++ b/langgraph/graph/state.py
@@ -111,8 +111,13 @@ class StateGraph(Graph):
) -> CompiledGraph:
"""Compiles the state graph into a `CompiledGraph` object.
+ The compiled graph implements the `Runnable` interface and can be invoked,
+ streamed, batched, and run asynchronously.
+
Args:
checkpointer (Optional[BaseCheckpointSaver]): An optional checkpoint saver object.
+ This serves as a fully versioned "memory" for the graph, allowing
+ the graph to be paused and resumed, and replayed from any point.
interrupt_before (Optional[Sequence[str]]): An optional list of node names to interrupt before.
interrupt_after (Optional[Sequence[str]]): An optional list of node names to interrupt after.
debug (bool): A flag indicating whether to enable debug mode.
diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py
index 74a6156b8..a28893837 100644
--- a/langgraph/prebuilt/agent_executor.py
+++ b/langgraph/prebuilt/agent_executor.py
@@ -4,6 +4,7 @@ from typing import Annotated, Sequence, TypedDict, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
+from langgraph._api.deprecation import deprecated
from langgraph.graph import END, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt.tool_executor import ToolExecutor
@@ -40,6 +41,15 @@ def _get_agent_state(input_schema=None):
return AgentState
+@deprecated(
+ "0.0.44",
+ alternative="create_tool_calling_executor",
+ example="""
+from langgraph.prebuilt import chat_agent_executor
+
+chat_agent_executor.create_tool_calling_executor(...)
+""",
+)
def create_agent_executor(
agent_runnable, tools, input_schema=None
) -> CompiledStateGraph:
@@ -53,32 +63,24 @@ def create_agent_executor(
Returns:
The `CompiledStateGraph` object.
+
Examples:
- from langgraph.prebuilt import create_agent_executor
+ # Since this is deprecated, you should use `create_tool_calling_executor` instead.
+ # Example usage:
+ from langgraph.prebuilt import chat_agent_executor
from langchain_openai import ChatOpenAI
- from langchain import hub
- from langchain.agents import create_openai_functions_agent
from langchain_community.tools.tavily_search import TavilySearchResults
tools = [TavilySearchResults(max_results=1)]
+ model = ChatOpenAI()
- # Get the prompt to use - you can modify this!
- prompt = hub.pull("hwchase17/openai-functions-agent")
+ app = chat_agent_executor.create_tool_calling_executor(model, tools)
- # Choose the LLM that will drive the agent
- llm = ChatOpenAI(model="gpt-3.5-turbo-1106")
-
- # Construct the OpenAI Functions agent
- agent_runnable = create_openai_functions_agent(llm, tools, prompt)
-
- app = create_agent_executor(agent_runnable, tools)
-
- inputs = {"input": "what is the weather in sf", "chat_history": []}
+ inputs = {"messages": [("user", "what is the weather in sf")]}
for s in app.stream(inputs):
print(list(s.values())[0])
print("----")
-
"""
if isinstance(tools, ToolExecutor):
diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py
index 4632e3488..408c1b4cc 100644
--- a/langgraph/prebuilt/chat_agent_executor.py
+++ b/langgraph/prebuilt/chat_agent_executor.py
@@ -7,6 +7,7 @@ from langchain_core.runnables import Runnable, RunnableLambda
from langchain_core.tools import BaseTool
from langchain_core.utils.function_calling import convert_to_openai_function
+from langgraph._api.deprecation import deprecated
from langgraph.checkpoint import BaseCheckpointSaver
from langgraph.graph import END, StateGraph
from langgraph.graph.graph import CompiledGraph
@@ -25,9 +26,30 @@ class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
+@deprecated("0.0.44", "create_tool_calling_executor")
def create_function_calling_executor(
model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]]
) -> CompiledGraph:
+ """Creates a graph that works with a chat model that utilizes function calling.
+
+ Examples:
+
+ # Since this is deprecated, you should use `create_tool_calling_executor` instead.
+ # Example usage:
+ from langgraph.prebuilt import chat_agent_executor
+ from langchain_openai import ChatOpenAI
+ from langchain_community.tools.tavily_search import TavilySearchResults
+
+ tools = [TavilySearchResults(max_results=1)]
+ model = ChatOpenAI()
+
+ app = chat_agent_executor.create_tool_calling_executor(model, tools)
+
+ inputs = {"messages": [("user", "what is the weather in sf")]}
+ for s in app.stream(inputs):
+ print(list(s.values())[0])
+ print("----")
+ """
if isinstance(tools, ToolExecutor):
tool_executor = tools
tool_classes = tools.tools
@@ -165,17 +187,17 @@ def create_tool_calling_executor(
Examples:
- from langgraph.prebuilt import chat_agent_executor
- from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
- from langchain_core.messages import HumanMessage
+ from langchain_openai import ChatOpenAI
+
+ from langgraph.prebuilt import chat_agent_executor
tools = [TavilySearchResults(max_results=1)]
model = ChatOpenAI()
app = chat_agent_executor.create_tool_calling_executor(model, tools)
- inputs = {"messages": [HumanMessage(content="what is the weather in sf")]}
+ inputs = {"messages": [("user", "what is the weather in sf")]}
for s in app.stream(inputs):
print(list(s.values())[0])
print("----")
diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py
index d838579b2..38b93bef0 100644
--- a/langgraph/prebuilt/tool_executor.py
+++ b/langgraph/prebuilt/tool_executor.py
@@ -13,22 +13,71 @@ INVALID_TOOL_MSG_TEMPLATE = (
class ToolInvocationInterface:
- """Interface for invoking a tool"""
+ """Interface for invoking a tool.
+
+ Attributes:
+ tool (str): The name of the tool to invoke.
+ tool_input (Union[str, dict]): The input to pass to the tool.
+
+ """
tool: str
tool_input: Union[str, dict]
class ToolInvocation(Serializable):
- """Information about how to invoke a tool."""
+ """Information about how to invoke a tool.
+
+ Attributes:
+ tool (str): The name of the Tool to execute.
+ tool_input (Union[str, dict]): The input to pass in to the Tool.
+
+ Examples:
+
+ invocation = ToolInvocation(
+ tool="search",
+ tool_input="What is the capital of France?"
+ )
+ """
tool: str
- """The name of the Tool to execute."""
tool_input: Union[str, dict]
- """The input to pass in to the Tool."""
class ToolExecutor(RunnableCallable):
+ """Executes a tool invocation.
+
+ Args:
+ tools (Sequence[BaseTool]): A sequence of tools that can be invoked.
+ invalid_tool_msg_template (str, optional): The template for the error message
+ when an invalid tool is requested. Defaults to INVALID_TOOL_MSG_TEMPLATE.
+
+ Examples:
+
+ from langchain_core.tools import tool
+ from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
+
+
+ @tool
+ def search(query: str) -> str:
+ \"\"\"Search engine.\"\"\"
+ return f"Searching for: {query}"
+
+
+ tools = [search]
+ executor = ToolExecutor(tools)
+
+ invocation = ToolInvocation(tool="search", tool_input="What is the capital of France?")
+ result = executor.invoke(invocation)
+ print(result) # Output: "Searching for: What is the capital of France?"
+
+ invocation = ToolInvocation(
+ tool="nonexistent", tool_input="What is the capital of France?"
+ )
+ result = executor.invoke(invocation)
+ print(result) # Output: "nonexistent is not a valid tool, try one of [search]."
+ """
+
def __init__(
self,
tools: Sequence[BaseTool],
diff --git a/langgraph/prebuilt/tool_node.py b/langgraph/prebuilt/tool_node.py
index 4d4b283b1..89fdb2104 100644
--- a/langgraph/prebuilt/tool_node.py
+++ b/langgraph/prebuilt/tool_node.py
@@ -113,41 +113,41 @@ def tools_condition(
Examples:
- .. code-block:: python
- from langchain_anthropic import ChatAnthropic
- from langchain_core.tools import tool
+ from langchain_anthropic import ChatAnthropic
+ from langchain_core.tools import tool
- from langgraph.graph import MessageGraph
- from langgraph.prebuilt import ToolNode, tools_condition
+ from langgraph.graph import MessageGraph
+ from langgraph.prebuilt import ToolNode, tools_condition
- @tool
- def divide(a: float, b: float) -> int:
- \"\"\"Return a / b.\"\"\"
- return a / b
+ @tool
+ def divide(a: float, b: float) -> int:
+ \"\"\"Return a / b.\"\"\"
+ return a / b
- llm = ChatAnthropic(model="claude-3-haiku-20240307")
- tools = [divide]
+ llm = ChatAnthropic(model="claude-3-haiku-20240307")
+ tools = [divide]
- graph_builder = MessageGraph()
- graph_builder.add_node("tools", ToolNode(tools))
- graph_builder.add_node("chatbot", llm.bind_tools(tools))
- graph_builder.add_edge("tools", "chatbot")
- graph_builder.add_conditional_edges(
- "chatbot",
- tools_condition,
- {
- # If it returns 'action', route to the 'tools' node
- "action": "tools",
- # If it returns '__end__', route to the end
- "__end__": "__end__",
- },
- )
- graph_builder.set_entry_point("chatbot")
- graph = graph_builder.compile()
- graph.invoke([("user", "What's 329993 divided by 13662?")])
+ graph_builder = MessageGraph()
+ graph_builder.add_node("tools", ToolNode(tools))
+ graph_builder.add_node("chatbot", llm.bind_tools(tools))
+ graph_builder.add_edge("tools", "chatbot")
+ graph_builder.add_conditional_edges(
+ "chatbot",
+ # highlight-next-line
+ tools_condition,
+ {
+ # If it returns 'action', route to the 'tools' node
+ "action": "tools",
+ # If it returns '__end__', route to the end
+ "__end__": "__end__",
+ },
+ )
+ graph_builder.set_entry_point("chatbot")
+ graph = graph_builder.compile()
+ graph.invoke([("user", "What's 329993 divided by 13662?")])
"""
if isinstance(state, list):
ai_message = state[-1]