From 21b8cbfd33c824b215cb1f51bcab96a69fbae2d6 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Sat, 4 May 2024 00:17:54 -0700 Subject: [PATCH] Deprecate Chat agent executor & Function Calling Executor in Docs (#392) --- docs/docs/reference/checkpoints.md | 30 ++ docs/docs/reference/graphs.md | 7 +- docs/docs/reference/prebuilt.md | 11 +- examples/agent_executor/high-level.ipynb | 339 +--------------- .../high-level.ipynb | 108 +----- examples/visualization.ipynb | 367 ++++++++---------- langgraph/_api/__init__.py | 0 langgraph/_api/deprecation.py | 34 ++ langgraph/checkpoint/aiosqlite.py | 162 ++++++-- langgraph/checkpoint/memory.py | 85 ++++ langgraph/checkpoint/sqlite.py | 228 +++++++++-- langgraph/graph/message.py | 95 ++++- langgraph/graph/state.py | 5 + langgraph/prebuilt/agent_executor.py | 32 +- langgraph/prebuilt/chat_agent_executor.py | 30 +- langgraph/prebuilt/tool_executor.py | 57 ++- langgraph/prebuilt/tool_node.py | 56 +-- 17 files changed, 877 insertions(+), 769 deletions(-) create mode 100644 langgraph/_api/__init__.py create mode 100644 langgraph/_api/deprecation.py 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]