Compare commits

..
30 Commits
Author SHA1 Message Date
Nuno Campos 2b70dba0e0 0.2.58 2024-12-10 14:09:11 -08:00
Nuno CamposandGitHub dc0398efd1 Merge pull request #2661 from langchain-ai/nc/5dec/perf
lib: Performance improvements
2024-12-10 14:04:02 -08:00
David DuongandGitHub 02f1904ba7 Merge pull request #2699 from langchain-ai/dqbd/sdk-js-0.0.32
feat(sdk-js): bump to 0.0.32
2024-12-11 02:00:21 +04:00
Nuno Campos 30f852e7b2 Fix 2024-12-10 13:56:26 -08:00
Tat Dat Duong 7fc6c4b1fa feat(sdk-js): bump to 0.0.32 2024-12-10 22:52:51 +01:00
Nuno Campos 7f8ec2c590 Fix 2024-12-10 13:46:45 -08:00
Vadym BardaandGitHub 611588613d docs: add an FAQ note for command vs cond edge (#2697) 2024-12-10 15:16:02 -05:00
Nuno Campos 11e80210a2 lib: Performance improvements
- don't create contextvars.Context/asyncio.Task in RunnableSeq (not needed as each step creates it if necessary)
- don't run in-memory-saver methods in background threads (no point as they hold the gil)
- avoid calling should_interrupt when no interrupts set
2024-12-10 11:40:24 -08:00
Nuno CamposandGitHub 60d742ea48 Merge pull request #2683 from langchain-ai/nc/9dec/invoke-command-goto
lib: Add support for invoke(Command(goto=<str>))
2024-12-10 11:39:10 -08:00
Nuno Campos a7ac9ffd4e Update test 2024-12-10 11:31:41 -08:00
Vadym BardaandGitHub 3d97b97c86 fix typo (#2696) 2024-12-10 14:19:23 -05:00
Nuno CamposandGitHub a7d1ecbb74 Merge pull request #2693 from langchain-ai/eugene/fix_test
langgraph[patch]: Fix unit test for Command(update)
2024-12-10 11:09:11 -08:00
Eugene YurtsevandNuno Campos 7cabc0a3dc reformat 2024-12-10 11:04:07 -08:00
Eugene YurtsevandNuno Campos f9cdfd3ac4 x 2024-12-10 11:03:56 -08:00
Eugene YurtsevandNuno Campos dd778f8ed6 qxqx 2024-12-10 11:03:56 -08:00
Nuno Campos df5d08f689 Fix 2024-12-10 11:03:01 -08:00
Nuno Campos a9b94f93ee Update again 2024-12-10 11:03:01 -08:00
Nuno Campos 5f869b9e75 Update test 2024-12-10 11:03:01 -08:00
Nuno Campos 79562f3f37 lib: Add support for invoke(Command(goto=<str>)) 2024-12-10 11:03:01 -08:00
Nuno Campos 081b2cbdcf Fix 2024-12-10 11:01:25 -08:00
Eugene YurtsevandNuno Campos 70eeb2a670 x 2024-12-10 11:00:58 -08:00
Nuno CamposandGitHub 0f287d986b Merge pull request #2695 from langchain-ai/nc/10dec/multistep-plan
lib: Add unit test for multistep planner graph
2024-12-10 10:54:37 -08:00
Nuno CamposandGitHub 1fd9da6718 Merge pull request #2691 from langchain-ai/dqbd/enhanced-config-type-extraction
fix(config): extract default values, description from pydantic models, typeddict and dataclass
2024-12-10 10:44:24 -08:00
Nuno Campos ef6c5b4711 lib: Add unit test for multistep planner graph 2024-12-10 10:40:49 -08:00
Vadym BardaandGitHub 70a5ef6713 docs: small updates (#2694) 2024-12-10 12:02:24 -05:00
Tat Dat Duong 17c1a8db46 Fix lint 2024-12-10 17:42:01 +01:00
Vadym BardaandGitHub 97a51014c3 docs: add a how-to on updating state from tools (#2670) 2024-12-10 11:20:50 -05:00
Tat Dat Duong 5a30fc6a87 Handle PydanticUndefined, add tests 2024-12-10 17:06:37 +01:00
Tat Dat Duong 1f68bd0d83 Move to langgraph.utils.fields 2024-12-10 16:49:05 +01:00
Tat Dat Duong b4f11929f8 fix(config): extract default values, description from pydantic models, typeddict and dataclass 2024-12-10 15:24:56 +01:00
19 changed files with 805 additions and 102 deletions
+6
View File
@@ -353,6 +353,12 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
Check out this [how-to guide](../how-tos/command.ipynb) for an end-to-end example of how to use `Command`.
### When should I use Command instead of conditional edges?
Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](./multi_agent.md#handoffs) where it's important to route to a different agent and pass some information to that agent.
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
### Using inside tools
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool:
+1 -1
View File
@@ -153,7 +153,7 @@ network = builder.compile()
### Supervisor
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [conditional edges](./low_level.md#conditional-edges) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](./low_level.md#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
```python
from typing import Literal
+1
View File
@@ -81,6 +81,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to handle tool calling errors](tool-calling-errors.ipynb)
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
- [How to pass config to tools](pass-config-to-tools.ipynb)
- [How to update graph state from tools](update-state-from-tools.ipynb)
- [How to handle large numbers of tools](many-tools.ipynb)
### Subgraphs
+7 -4
View File
@@ -254,7 +254,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'travel_advisor': {'messages': {'role': 'ai', 'content': 'The Caribbean is a fantastic choice for warm, sunny weather and beautiful beaches. Here are a few destinations you might consider:\\n\\n1. **Jamaica**: Known for its vibrant culture, reggae music, and stunning beaches like Negril and Montego Bay.\\n\\n2. **Bahamas**: With over 700 islands, the Bahamas offers clear turquoise waters and beautiful sandy beaches, perfect for relaxation and water sports.\\n\\n3. **Dominican Republic**: Known for its resorts, beaches, and golfing. Punta Cana and Puerto Plata are popular destinations.\\n\\n4. **Barbados**: Offers beautiful beaches and a rich history, with plenty of activities and festivals.\\n\\n5. **Puerto Rico**: A mix of Spanish heritage and modern resorts, with opportunities for hiking in El Yunque National Forest and enjoying the vibrant nightlife in San Juan.\\n\\n6. **Aruba**: Known for its dry climate and sunny days, with beautiful beaches and activities like snorkeling and diving.\\n\\nEach of these destinations has its own unique charm and appeal. If you need specific sightseeing or hotel recommendations, let me know!', 'name': 'travel_advisor'}}}\n",
"{'travel_advisor': {'messages': {'role': 'ai', 'content': 'The Caribbean offers many warm destinations perfect for a relaxing getaway. Consider visiting Jamaica for its beautiful beaches and vibrant culture, the Bahamas for its stunning islands and clear waters, or the Dominican Republic for its all-inclusive resorts and rich history. Let me know if you need more information on sightseeing or hotel recommendations!', 'name': 'travel_advisor'}}}\n",
"\n",
"\n"
]
@@ -286,10 +286,13 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'travel_advisor': {'messages': {'role': 'ai', 'content': \"I recommend visiting Barbados for a warm Caribbean getaway. It's known for its beautiful beaches, vibrant culture, and friendly locals. Let me gather some sightseeing and hotel recommendations for you.\", 'name': 'travel_advisor'}}}\n",
"{'travel_advisor': {'messages': {'role': 'ai', 'content': 'I recommend visiting Jamaica, a beautiful Caribbean island known for its warm climate, stunning beaches, and vibrant culture.', 'name': 'travel_advisor'}}}\n",
"\n",
"\n",
"{'sightseeing_advisor': {'messages': {'role': 'ai', 'content': \"Barbados is a fantastic destination to experience the warmth of the Caribbean. Here are some things to do and places to stay:\\n\\n### Sightseeing Recommendations:\\n1. **Harrison's Cave**: Explore this stunning limestone cave with its impressive stalactites and stalagmites. \\n2. **Bathsheba Beach**: Known for its unique rock formations and surf-friendly waves, it's perfect for a day of relaxation and exploration.\\n3. **St. Nicholas Abbey**: Visit this historic plantation house, distillery, and museum for a glimpse into the island's colonial past.\\n4. **Animal Flower Cave**: Located at the northern tip of Barbados, this sea cave offers incredible views and natural rock pools.\\n5. **Oistins Fish Fry**: Experience local culture and cuisine with fresh seafood, music, and dancing every Friday night.\\n\\n### Hotel Recommendations:\\n1. **Sandy Lane**: A luxurious resort offering world-class amenities, golf courses, and a private beach.\\n2. **The Crane Resort**: Known for its historic charm and stunning ocean views, it provides a unique blend of luxury and culture.\\n3. **Sea Breeze Beach House**: Offers an all-inclusive experience with multiple dining options and beachfront access.\\n4. **The House by Elegant Hotels**: A boutique, adults-only hotel perfect for a romantic getaway with personalized service and beachfront location.\\n\\nEnjoy your trip to Barbados!\", 'name': 'sightseeing_advisor'}}}\n",
"{'sightseeing_advisor': {'messages': {'role': 'ai', 'content': \"Jamaica is a fantastic choice for a warm Caribbean getaway. Here are some top things to do while you're there:\\n\\n1. **Dunn's River Falls**: Located near Ocho Rios, this is one of Jamaica's most famous waterfalls. You can climb the falls, swim in the refreshing pools, or simply enjoy the beautiful surroundings.\\n\\n2. **Seven Mile Beach**: Located in Negril, this is one of the most beautiful beaches in the Caribbean. It's perfect for sunbathing, swimming, and enjoying water sports.\\n\\n3. **Bob Marley Museum**: Situated in Kingston, this museum is dedicated to the life and legacy of the reggae legend Bob Marley and is a must-visit for music lovers.\\n\\n4. **Blue Mountains**: Go hiking or take a tour to explore the Blue Mountains, where you can enjoy breathtaking views and taste some of the world's best coffee.\\n\\n5. **Luminous Lagoon**: Experience the natural wonder of the Luminous Lagoon in Falmouth, where the water glows at night due to bioluminescent microorganisms.\\n\\nFor hotel recommendations, I suggest checking with a hotel advisor for the best options that suit your budget and preferences.\", 'name': 'sightseeing_advisor'}}}\n",
"\n",
"\n",
"{'hotel_advisor': {'messages': {'role': 'ai', 'content': 'For hotel recommendations in Jamaica, here are a few options across different areas: \\n\\n1. **Sandals Montego Bay** (Montego Bay): A luxurious all-inclusive resort ideal for couples, offering beautiful beachfront views and a variety of dining options.\\n\\n2. **Half Moon Resort** (Montego Bay): A family-friendly resort with a private beach, golf course, and various activities for all ages.\\n\\n3. **Jamaica Inn** (Ocho Rios): A charming boutique hotel known for its excellent service and tranquil atmosphere.\\n\\n4. **The Caves** (Negril): A unique and romantic cliff-side resort offering stunning ocean views and intimate dining experiences.\\n\\n5. **Trident Hotel** (Port Antonio): A luxurious and contemporary hotel offering privacy, elegance, and beautiful views of the Caribbean Sea.\\n\\nThese options cater to different tastes and budgets, ensuring a comfortable and enjoyable stay in Jamaica.', 'name': 'hotel_advisor'}}}\n",
"\n",
"\n"
]
@@ -558,7 +561,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
"version": "3.11.9"
}
},
"nbformat": 4,
@@ -0,0 +1,383 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7c58c957-83d8-44ff-8580-a9b3dd39a0a9",
"metadata": {},
"source": [
"# How to update graph state from tools"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "95f30587-8dd2-40be-920d-59539089c09f",
"metadata": {},
"source": [
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Command](../../concepts/low_level/#command)\n",
"\n",
"A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={\"my_custom_key\": \"foo\", \"messages\": [...]})` from the tool:\n",
"\n",
"```python\n",
"@tool\n",
"def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):\n",
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
" user_info = get_user_info(config)\n",
" return Command(\n",
" update={\n",
" # update the state keys\n",
" \"user_info\": user_info,\n",
" # update the message history\n",
" \"messages\": [ToolMessage(\"Successfully looked up user information\", tool_call_id=tool_call_id)]\n",
" }\n",
" )\n",
"```\n",
"\n",
"!!! important\n",
"\n",
" If you want to use tools that return `Command` and update graph state, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:\n",
" \n",
" ```python\n",
" def call_tools(state):\n",
" ...\n",
" commands = [tools_by_name[call[\"name\"].invoke(call, config={\"coerce_tool_content\": False}) for tool_call in tool_calls]\n",
" return commands\n",
" ```\n",
"\n",
"This guide shows how you can do this using LangGraph's prebuilt components ([`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode]).\n",
"\n",
"!!! note\n",
"\n",
" Support for tools that return [`Command`][langgraph.types.Command] was added in LangGraph `v0.2.57`.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "64500eca-1cdc-43d9-9401-f4cd9999881f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a3f92fb2-9175-47fa-9c7d-ad5f44bfd20e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Please provide your OPENAI_API_KEY ········\n"
]
}
],
"source": [
"import os\n",
"import getpass\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "caf6ff9f-c1e6-499e-a230-9fa231ea7d2f",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "10e9a9c6-fa3f-416c-bac0-3e58d7259908",
"metadata": {},
"source": [
"Let's create a simple ReAct style agent that can look up user information and personalize the response based on the user info."
]
},
{
"cell_type": "markdown",
"id": "4255b9b9-cf67-4cc3-8018-1708f5dfcfd2",
"metadata": {},
"source": [
"## Define tool"
]
},
{
"cell_type": "markdown",
"id": "7de6b010-aab1-4fe8-8251-907fcae78583",
"metadata": {},
"source": [
"First, let's define the tool that we'll be using to look up user information. We'll use a naive implementation that simply looks user information up using a dictionary:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8d070c9f-6e61-4724-85dc-ac4531b9c79a",
"metadata": {},
"outputs": [],
"source": [
"USER_INFO = [\n",
" {\"user_id\": \"1\", \"name\": \"Bob Dylan\", \"location\": \"New York, NY\"},\n",
" {\"user_id\": \"2\", \"name\": \"Taylor Swift\", \"location\": \"Beverly Hills, CA\"},\n",
"]\n",
"\n",
"USER_ID_TO_USER_INFO = {info[\"user_id\"]: info for info in USER_INFO}"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "08d1ecca-ee57-4e97-b8d0-e09de85337d4",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt.chat_agent_executor import AgentState\n",
"from langgraph.types import Command\n",
"from langchain_core.tools import tool\n",
"from langchain_core.tools.base import InjectedToolCallId\n",
"from langchain_core.messages import ToolMessage\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"from typing_extensions import Any, Annotated\n",
"\n",
"\n",
"class State(AgentState):\n",
" # user provided\n",
" last_name: str\n",
" # updated by the tool\n",
" user_info: dict[str, Any]\n",
"\n",
"\n",
"@tool\n",
"def lookup_user_info(\n",
" tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig\n",
"):\n",
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
" user_id = config.get(\"configurable\", {}).get(\"user_id\")\n",
" if user_id is None:\n",
" raise ValueError(\"Please provide user ID\")\n",
"\n",
" if user_id not in USER_ID_TO_USER_INFO:\n",
" raise ValueError(f\"User '{user_id}' not found\")\n",
"\n",
" user_info = USER_ID_TO_USER_INFO[user_id]\n",
" return Command(\n",
" update={\n",
" # update the state keys\n",
" \"user_info\": user_info,\n",
" # update the message history\n",
" \"messages\": [\n",
" ToolMessage(\n",
" \"Successfully looked up user information\", tool_call_id=tool_call_id\n",
" )\n",
" ],\n",
" }\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "b99e5f24-5e5e-4a34-baae-467182675bb5",
"metadata": {},
"source": [
"## Define prompt"
]
},
{
"cell_type": "markdown",
"id": "cbb06aea-6654-4245-91f8-af6e8f2b5377",
"metadata": {},
"source": [
"Let's now add personalization: we'll respond differently to the user based on the state values AFTER the state has been updated from the tool. To achieve this, let's define a function that will dynamically construct the system prompt based on the graph state. It will be called ever time the LLM is called and the function output will be passed to the LLM:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "c553d062-d145-4145-84bd-9b798f7c95c2",
"metadata": {},
"outputs": [],
"source": [
"def state_modifier(state: State):\n",
" user_info = state.get(\"user_info\")\n",
" if user_info is None:\n",
" return state[\"messages\"]\n",
"\n",
" system_msg = (\n",
" f\"User name is {user_info['name']}. User lives in {user_info['location']}\"\n",
" )\n",
" return [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]"
]
},
{
"cell_type": "markdown",
"id": "c5acdd5d-68be-466b-9c21-46cbed91d2bc",
"metadata": {},
"source": [
"## Define graph"
]
},
{
"cell_type": "markdown",
"id": "afb65028-0359-46c8-b09c-ffc90180f759",
"metadata": {},
"source": [
"Finally, let's combine this into a single graph using the prebuilt `create_react_agent`:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "2d59db29-fd51-4d29-9854-21763a4855e3",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\")\n",
"\n",
"agent = create_react_agent(\n",
" model,\n",
" # pass the tool that can update state\n",
" [lookup_user_info],\n",
" state_schema=State,\n",
" # pass dynamic prompt function\n",
" state_modifier=state_modifier,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "0782b8ab-a603-47b8-9a76-77f593402678",
"metadata": {},
"source": [
"## Use it!"
]
},
{
"cell_type": "markdown",
"id": "6165e153-ab28-4404-adea-796c7bd0701b",
"metadata": {},
"source": [
"Let's now try running our agent. We'll need to provide user ID in the config so that our tool knows what information to look up:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "de34a58b-1765-4b63-a232-d46790aff884",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-57eeb216-e35d-4501-aaac-b5c6b26fb17c-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n",
"{'tools': {'user_info': {'user_id': '1', 'name': 'Bob Dylan', 'location': 'New York, NY'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='168d8ff8-b021-4c8b-a11a-3b50c30a072c', tool_call_id='call_7LSUh6ZDvGJAUvlWvXiCK4Gf')]}}\n",
"\n",
"\n",
"{'agent': {'messages': [AIMessage(content=\"Hi Bob! Since you're in New York, NY, there are plenty of exciting things to do over the weekend. Here are some suggestions:\\n\\n1. **Explore Central Park**: Take a leisurely walk, rent a bike, or have a picnic in this iconic park.\\n\\n2. **Visit a Museum**: Check out The Metropolitan Museum of Art or the Museum of Modern Art (MoMA) for an enriching cultural experience.\\n\\n3. **Broadway Show**: Catch a Broadway show or an off-Broadway performance for some world-class entertainment.\\n\\n4. **Food Tour**: Explore different neighborhoods like Greenwich Village or Williamsburg for diverse culinary experiences.\\n\\n5. **Brooklyn Bridge Walk**: Take a walk across the Brooklyn Bridge for stunning views of the city skyline.\\n\\n6. **Visit a Rooftop Bar**: Enjoy a drink with a view at one of New Yorks many rooftop bars.\\n\\n7. **Explore a New Neighborhood**: Discover the unique charm of areas like SoHo, Chelsea, or Astoria.\\n\\n8. **Live Music**: Check out live music venues for a night of great performances.\\n\\n9. **Art Galleries**: Visit some of the smaller art galleries around Chelsea or the Lower East Side.\\n\\n10. **Attend a Local Event**: Look up any local events or festivals happening this weekend.\\n\\nFeel free to let me know if you want more details on any of these activities!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 285, 'prompt_tokens': 95, 'total_tokens': 380, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'stop', 'logprobs': None}, id='run-f13ce15b-02b6-40e6-8264-c4d9edd0d03a-0', usage_metadata={'input_tokens': 95, 'output_tokens': 285, 'total_tokens': 380, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in agent.stream(\n",
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
" # provide user ID in the config\n",
" {\"configurable\": {\"user_id\": \"1\"}},\n",
"):\n",
" print(chunk)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "d9b2281f-269c-41dd-b6b2-4c743f11ffc9",
"metadata": {},
"source": [
"We can see that the model correctly recommended some New York activities for Bob Dylan! Let's try getting recommendations for Taylor Swift:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "9d71af94-572a-4961-88a7-665e792cf96a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-bacacd7d-76cc-4f6b-9e9b-d9e6f00b9391-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n",
"{'tools': {'user_info': {'user_id': '2', 'name': 'Taylor Swift', 'location': 'Beverly Hills, CA'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='d81ef31e-6d77-4f13-ae86-e2e6ba567e3d', tool_call_id='call_5HLtJtzcgmKbtmK6By21wW5Y')]}}\n",
"\n",
"\n",
"{'agent': {'messages': [AIMessage(content=\"Hi Taylor! Since you're in Beverly Hills, here are a few suggestions for a fun weekend:\\n\\n1. **Hiking at Runyon Canyon**: Enjoy a scenic hike with beautiful views of Los Angeles. It's a great way to get some exercise and enjoy the outdoors.\\n\\n2. **Visit Rodeo Drive**: Spend some time shopping or window shopping at the famous Rodeo Drive. You might even spot some celebrities!\\n\\n3. **Explore the Getty Center**: Check out the art collections and beautiful gardens at the Getty Center. The architecture and views are stunning.\\n\\n4. **Relax at a Spa**: Treat yourself to a relaxing day at one of Beverly Hills' luxurious spas.\\n\\n5. **Dining Out**: Try a new restaurant or visit your favorite spot for a delicious meal. Beverly Hills has a fantastic dining scene.\\n\\n6. **Attend a Local Event**: Check out any local events or concerts happening this weekend. Beverly Hills often hosts exciting events.\\n\\nEnjoy your weekend!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 198, 'prompt_tokens': 95, 'total_tokens': 293, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'stop', 'logprobs': None}, id='run-2057df76-f192-4c69-a66a-1f0a86bf5d66-0', usage_metadata={'input_tokens': 95, 'output_tokens': 198, 'total_tokens': 293, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in agent.stream(\n",
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
" {\"configurable\": {\"user_id\": \"2\"}},\n",
"):\n",
" print(chunk)\n",
" print(\"\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+1
View File
@@ -192,6 +192,7 @@ nav:
- how-tos/tool-calling.ipynb
- how-tos/tool-calling-errors.ipynb
- how-tos/pass-run-time-values-to-tools.ipynb
- how-tos/update-state-from-tools.ipynb
- how-tos/pass-config-to-tools.ipynb
- how-tos/many-tools.ipynb
- Subgraphs:
@@ -1,4 +1,3 @@
import asyncio
import logging
import os
import pickle
@@ -6,7 +5,6 @@ import random
import shutil
from collections import defaultdict
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
from functools import partial
from types import TracebackType
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
@@ -395,9 +393,7 @@ class MemorySaver(
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
)
return self.get_tuple(config)
async def alist(
self,
@@ -418,24 +414,8 @@ class MemorySaver(
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
"""
loop = asyncio.get_running_loop()
iter = await loop.run_in_executor(
None,
partial(
self.list,
before=before,
limit=limit,
filter=filter,
),
config,
)
while True:
# handling StopIteration exception inside coroutine won't work
# as expected, so using next() with default value to break the loop
if item := await loop.run_in_executor(None, next, iter, None):
yield item
else:
break
for item in self.list(config, filter=filter, before=before, limit=limit):
yield item
async def aput(
self,
@@ -455,9 +435,7 @@ class MemorySaver(
Returns:
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
"""
return await asyncio.get_running_loop().run_in_executor(
None, self.put, config, checkpoint, metadata, new_versions
)
return self.put(config, checkpoint, metadata, new_versions)
async def aput_writes(
self,
@@ -474,10 +452,9 @@ class MemorySaver(
config (RunnableConfig): The config to associate with the writes.
writes (List[Tuple[str, Any]]): The writes to save, each as a (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
return self.put_writes(config, writes, task_id)
"""
return await asyncio.get_running_loop().run_in_executor(
None, self.put_writes, config, writes, task_id
)
return self.put_writes(config, writes, task_id)
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
if current is None:
+1
View File
@@ -559,6 +559,7 @@ class StateGraph(Graph):
for key, node in self.nodes.items():
compiled.attach_node(key, node)
compiled.attach_branch(START, SELF, CONTROL_BRANCH, with_reader=False)
for key, node in self.nodes.items():
compiled.attach_branch(key, SELF, CONTROL_BRANCH, with_reader=False)
+10 -3
View File
@@ -18,7 +18,6 @@ from typing import (
Type,
Union,
cast,
get_type_hints,
overload,
)
from uuid import UUID, uuid5
@@ -117,6 +116,7 @@ from langgraph.utils.config import (
patch_config,
patch_configurable,
)
from langgraph.utils.fields import get_enhanced_type_hints
from langgraph.utils.pydantic import create_model
from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined]
@@ -319,8 +319,15 @@ class Pregel(PregelProtocol):
)
+ (
[
ConfigurableFieldSpec(id=name, annotation=typ)
for name, typ in get_type_hints(self.config_type).items()
ConfigurableFieldSpec(
id=name,
annotation=typ,
default=default,
description=description,
)
for name, typ, default, description in get_enhanced_type_hints(
self.config_type
)
]
if self.config_type is not None
else []
+8 -4
View File
@@ -14,6 +14,8 @@ from langgraph.constants import (
PUSH,
RESUME,
RETURN,
SELF,
START,
TAG_HIDDEN,
TASKS,
)
@@ -79,12 +81,14 @@ def map_command(
else:
sends = [cmd.goto]
for send in sends:
if not isinstance(send, Send):
if isinstance(send, Send):
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:{START}:{SELF}:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send, got {type(send).__name__}"
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
# TODO handle goto str for state graph
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
+20 -16
View File
@@ -311,7 +311,9 @@ class PregelLoop(LoopProtocol):
) -> Optional[PregelExecutableTask]:
"""Accept a PUSH from a task, potentially returning a new task to start."""
# don't start if we should interrupt *after* the original task
if should_interrupt(self.checkpoint, self.interrupt_after, [task]):
if self.interrupt_after and should_interrupt(
self.checkpoint, self.interrupt_after, [task]
):
self.to_interrupt.append(task)
return
if pushed := cast(
@@ -333,7 +335,9 @@ class PregelLoop(LoopProtocol):
),
):
# don't start if we should interrupt *before* the new task
if should_interrupt(self.checkpoint, self.interrupt_before, [pushed]):
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, [pushed]
):
self.to_interrupt.append(pushed)
return
# produce debug output
@@ -409,7 +413,7 @@ class PregelLoop(LoopProtocol):
}
)
# after execution, check if we should interrupt
if should_interrupt(
if self.interrupt_after and should_interrupt(
self.checkpoint, self.interrupt_after, self.tasks.values()
):
self.status = "interrupt_after"
@@ -422,18 +426,6 @@ class PregelLoop(LoopProtocol):
self.status = "out_of_steps"
return False
# apply NULL writes
if null_writes := [
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
]:
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
# prepare next tasks
self.tasks = prepare_next_tasks(
self.checkpoint,
@@ -493,7 +485,7 @@ class PregelLoop(LoopProtocol):
return self.tick(input_keys=input_keys)
# before execution, check if we should interrupt
if should_interrupt(
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, self.tasks.values()
):
self.status = "interrupt_before"
@@ -552,6 +544,18 @@ class PregelLoop(LoopProtocol):
# save writes
for tid, ws in writes.items():
self.put_writes(tid, ws)
# apply NULL writes
if null_writes := [
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
]:
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
# proceed past previous checkpoint
if is_resuming:
self.checkpoint["versions_seen"].setdefault(INTERRUPT, {})
+42 -1
View File
@@ -1,5 +1,5 @@
import dataclasses
from typing import Any, Optional, Type, Union
from typing import Any, Generator, Optional, Type, Union, get_type_hints
from typing_extensions import Annotated, NotRequired, ReadOnly, Required, get_origin
@@ -106,3 +106,44 @@ def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any:
if _is_optional_type(type_):
return None
return ...
def get_enhanced_type_hints(
type: Type[Any],
) -> Generator[tuple[str, Any, Any, Optional[str]], None, None]:
"""Attempt to extract default values and descriptions from provided type, used for config schema."""
for name, typ in get_type_hints(type).items():
default = None
description = None
# Pydantic models
try:
if hasattr(type, "__fields__") and name in type.__fields__:
field = type.__fields__[name]
if hasattr(field, "description") and field.description is not None:
description = field.description
if hasattr(field, "default") and field.default is not None:
default = field.default
if (
hasattr(default, "__class__")
and getattr(default.__class__, "__name__", "")
== "PydanticUndefinedType"
):
default = None
except (AttributeError, KeyError, TypeError):
pass
# TypedDict, dataclass
try:
if hasattr(type, "__dict__"):
type_dict = getattr(type, "__dict__")
if name in type_dict:
default = type_dict[name]
except (AttributeError, KeyError, TypeError):
pass
yield name, typ, default, description
+4 -12
View File
@@ -404,12 +404,10 @@ class RunnableSeq(Runnable):
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
context = copy_context()
context.run(_set_config_context, config)
if i == 0:
input = context.run(step.invoke, input, config, **kwargs)
input = step.invoke(input, config, **kwargs)
else:
input = context.run(step.invoke, input, config)
input = step.invoke(input, config)
# finish the root run
except BaseException as e:
run_manager.on_chain_error(e)
@@ -443,16 +441,10 @@ class RunnableSeq(Runnable):
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
context = copy_context()
context.run(_set_config_context, config)
if i == 0:
coro = step.ainvoke(input, config, **kwargs)
input = await step.ainvoke(input, config, **kwargs)
else:
coro = step.ainvoke(input, config)
if ASYNCIO_ACCEPTS_CONTEXT:
input = await asyncio.create_task(coro, context=context)
else:
input = await asyncio.create_task(coro)
input = await step.ainvoke(input, config)
# finish the root run
except BaseException as e:
await run_manager.on_chain_error(e)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.57"
version = "0.2.58"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+113 -15
View File
@@ -7602,7 +7602,7 @@ def test_root_graph(
content="result for query",
name="search_api",
tool_call_id="tool_call123",
id="00000000-0000-4000-8000-000000000033",
id="00000000-0000-4000-8000-000000000037",
)
]
},
@@ -7625,7 +7625,7 @@ def test_root_graph(
content="result for another",
name="search_api",
tool_call_id="tool_call456",
id="00000000-0000-4000-8000-000000000041",
id="00000000-0000-4000-8000-000000000045",
)
]
},
@@ -8235,7 +8235,7 @@ def test_root_graph(
"__root__": [
HumanMessage(
content="what is weather in sf",
id="00000000-0000-4000-8000-000000000070",
id="00000000-0000-4000-8000-000000000078",
),
AIMessage(
content="",
@@ -8255,7 +8255,7 @@ def test_root_graph(
),
AIMessage(content="answer", id="ai2"),
AIMessage(
content="an extra message", id="00000000-0000-4000-8000-000000000092"
content="an extra message", id="00000000-0000-4000-8000-000000000100"
),
HumanMessage(content="what is weather in la"),
],
@@ -12901,7 +12901,7 @@ def test_send_to_nested_graphs(
metadata={
"step": 1,
"source": "loop",
"writes": {"edit": None},
"writes": None,
"parents": {"": AnyStr()},
"thread_id": "1",
"checkpoint_ns": AnyStr("generate_joke:"),
@@ -12946,7 +12946,7 @@ def test_send_to_nested_graphs(
metadata={
"step": 1,
"source": "loop",
"writes": {"edit": None},
"writes": None,
"parents": {"": AnyStr()},
"thread_id": "1",
"checkpoint_ns": AnyStr("generate_joke:"),
@@ -14906,9 +14906,14 @@ def test_dict_mixed_return() -> None:
assert graph.invoke({"foo": ""}) == {"foo": "ab"}
def test_command_with_static_breakpoints() -> None:
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_command_with_static_breakpoints(
request: pytest.FixtureRequest, checkpointer_name: str
) -> None:
"""Test that we can use Command to resume and update with static breakpoints."""
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
class State(TypedDict):
"""The graph state."""
@@ -14930,17 +14935,110 @@ def test_command_with_static_breakpoints() -> None:
builder.add_edge(START, "node1")
builder.add_edge("node1", "node2")
# A checkpointer must be enabled for interrupts to work!
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node1"])
config = {
"configurable": {
"thread_id": uuid.uuid4(),
}
}
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# Start the graph and interrupt at the first node
graph.invoke({"foo": "abc"}, config)
result = graph.invoke(Command(resume="node1"), config)
assert result == {"foo": "abc|node-1|node-2"}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_multistep_plan(request: pytest.FixtureRequest, checkpointer_name: str):
from langchain_core.messages import AnyMessage
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
class State(TypedDict, total=False):
plan: list[Union[str, list[str]]]
messages: Annotated[list[AnyMessage], add_messages]
def planner(state: State):
if state.get("plan") is None:
# create plan somehow
plan = ["step1", ["step2", "step3"], "step4"]
# pick the first step to execute next
first_step, *plan = plan
# put the rest of plan in state
return Command(goto=first_step, update={"plan": plan})
elif state["plan"]:
# go to the next step of the plan
next_step, *next_plan = state["plan"]
return Command(goto=next_step, update={"plan": next_plan})
else:
# the end of the plan
pass
def step1(state: State):
return Command(goto="planner", update={"messages": [("human", "step1")]})
def step2(state: State):
return Command(goto="planner", update={"messages": [("human", "step2")]})
def step3(state: State):
return Command(goto="planner", update={"messages": [("human", "step3")]})
def step4(state: State):
return Command(goto="planner", update={"messages": [("human", "step4")]})
builder = StateGraph(State)
builder.add_node(planner)
builder.add_node(step1)
builder.add_node(step2)
builder.add_node(step3)
builder.add_node(step4)
builder.add_edge(START, "planner")
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
assert graph.invoke({"messages": [("human", "start")]}, config) == {
"messages": [
_AnyIdHumanMessage(content="start"),
_AnyIdHumanMessage(content="step1"),
_AnyIdHumanMessage(content="step2"),
_AnyIdHumanMessage(content="step3"),
_AnyIdHumanMessage(content="step4"),
],
"plan": [],
}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_command_goto_with_static_breakpoints(
request: pytest.FixtureRequest, checkpointer_name: str
) -> None:
"""Use Command goto with static breakpoints."""
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
class State(TypedDict):
"""The graph state."""
foo: Annotated[str, operator.add]
def node1(state: State):
return {
"foo": "|node-1",
}
def node2(state: State):
return {
"foo": "|node-2",
}
builder = StateGraph(State)
builder.add_node("node1", node1)
builder.add_node("node2", node2)
builder.add_edge(START, "node1")
builder.add_edge("node1", "node2")
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node1"])
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# Start the graph and interrupt at the first node
graph.invoke({"foo": "abc"}, config)
result = graph.invoke(Command(goto=["node2"]), config)
assert result == {"foo": "abc|node-1|node-2|node-2"}
+132
View File
@@ -13199,3 +13199,135 @@ async def test_interrupt_loop(checkpointer_name: str):
] == [
{"node": {"age": 19}},
]
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_command_with_static_breakpoints(checkpointer_name: str) -> None:
"""Test that we can use Command to resume and update with static breakpoints."""
class State(TypedDict):
"""The graph state."""
foo: str
def node1(state: State):
return {
"foo": state["foo"] + "|node-1",
}
def node2(state: State):
return {
"foo": state["foo"] + "|node-2",
}
builder = StateGraph(State)
builder.add_node("node1", node1)
builder.add_node("node2", node2)
builder.add_edge(START, "node1")
builder.add_edge("node1", "node2")
async with awith_checkpointer(checkpointer_name) as checkpointer:
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node1"])
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# Start the graph and interrupt at the first node
await graph.ainvoke({"foo": "abc"}, config)
result = await graph.ainvoke(Command(update={"foo": "def"}), config)
assert result == {"foo": "def|node-1|node-2"}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_multistep_plan(checkpointer_name: str):
from langchain_core.messages import AnyMessage
class State(TypedDict, total=False):
plan: list[Union[str, list[str]]]
messages: Annotated[list[AnyMessage], add_messages]
def planner(state: State):
if state.get("plan") is None:
# create plan somehow
plan = ["step1", ["step2", "step3"], "step4"]
# pick the first step to execute next
first_step, *plan = plan
# put the rest of plan in state
return Command(goto=first_step, update={"plan": plan})
elif state["plan"]:
# go to the next step of the plan
next_step, *next_plan = state["plan"]
return Command(goto=next_step, update={"plan": next_plan})
else:
# the end of the plan
pass
def step1(state: State):
return Command(goto="planner", update={"messages": [("human", "step1")]})
def step2(state: State):
return Command(goto="planner", update={"messages": [("human", "step2")]})
def step3(state: State):
return Command(goto="planner", update={"messages": [("human", "step3")]})
def step4(state: State):
return Command(goto="planner", update={"messages": [("human", "step4")]})
builder = StateGraph(State)
builder.add_node(planner)
builder.add_node(step1)
builder.add_node(step2)
builder.add_node(step3)
builder.add_node(step4)
builder.add_edge(START, "planner")
async with awith_checkpointer(checkpointer_name) as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
assert await graph.ainvoke({"messages": [("human", "start")]}, config) == {
"messages": [
_AnyIdHumanMessage(content="start"),
_AnyIdHumanMessage(content="step1"),
_AnyIdHumanMessage(content="step2"),
_AnyIdHumanMessage(content="step3"),
_AnyIdHumanMessage(content="step4"),
],
"plan": [],
}
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_command_goto_with_static_breakpoints(checkpointer_name: str) -> None:
"""Use Command goto with static breakpoints."""
class State(TypedDict):
"""The graph state."""
foo: Annotated[str, operator.add]
def node1(state: State):
return {
"foo": "|node-1",
}
def node2(state: State):
return {
"foo": "|node-2",
}
builder = StateGraph(State)
builder.add_node("node1", node1)
builder.add_node("node2", node2)
builder.add_edge(START, "node1")
builder.add_edge("node1", "node2")
async with awith_checkpointer(checkpointer_name) as checkpointer:
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node1"])
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# Start the graph and interrupt at the first node
await graph.ainvoke({"foo": "abc"}, config)
result = await graph.ainvoke(Command(goto=["node2"]), config)
assert result == {"foo": "abc|node-1|node-2|node-2"}
+59 -1
View File
@@ -21,7 +21,11 @@ from typing_extensions import Annotated, NotRequired, Required
from langgraph.graph import END, StateGraph
from langgraph.graph.graph import CompiledGraph
from langgraph.utils.fields import _is_optional_type, get_field_default
from langgraph.utils.fields import (
_is_optional_type,
get_enhanced_type_hints,
get_field_default,
)
from langgraph.utils.runnable import is_async_callable, is_async_generator
pytestmark = pytest.mark.anyio
@@ -227,3 +231,57 @@ def test_is_required():
assert get_field_default("val_12", gcannos["val_12"], MyGrandChildDict) is None
assert get_field_default("val_9", gcannos["val_9"], MyGrandChildDict) is None
assert get_field_default("val_13", gcannos["val_13"], MyGrandChildDict) == ...
def test_enhanced_type_hints() -> None:
from dataclasses import dataclass
from typing import Annotated
from pydantic import BaseModel, Field
class MyTypedDict(TypedDict):
val_1: str
val_2: int = 42
val_3: str = "default"
hints = list(get_enhanced_type_hints(MyTypedDict))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", None)
@dataclass
class MyDataclass:
val_1: str
val_2: int = 42
val_3: str = "default"
hints = list(get_enhanced_type_hints(MyDataclass))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", None)
class MyPydanticModel(BaseModel):
val_1: str
val_2: int = 42
val_3: str = Field(default="default", description="A description")
hints = list(get_enhanced_type_hints(MyPydanticModel))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", "A description")
class MyPydanticModelWithAnnotated(BaseModel):
val_1: Annotated[str, Field(description="A description")]
val_2: Annotated[int, Field(default=42)]
val_3: Annotated[
str, Field(default="default", description="Another description")
]
hints = list(get_enhanced_type_hints(MyPydanticModelWithAnnotated))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, "A description")
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", "Another description")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.31",
"version": "0.0.32",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
Generated
+9 -14
View File
@@ -2933,13 +2933,13 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
[[package]]
name = "langchain-core"
version = "0.3.21"
version = "0.3.23"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
{file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
{file = "langchain_core-0.3.23-py3-none-any.whl", hash = "sha256:550c0b996990830fa6515a71a1192a8a0343367999afc36d4ede14222941e420"},
{file = "langchain_core-0.3.23.tar.gz", hash = "sha256:f9e175e3b82063cc3b160c2ca2b155832e1c6f915312e1204828f97d4aabf6e1"},
]
[package.dependencies]
@@ -3035,7 +3035,7 @@ langchain-core = ">=0.3.0,<0.4.0"
[[package]]
name = "langgraph"
version = "0.2.54"
version = "0.2.57"
description = "Building stateful, multi-actor applications with LLMs"
optional = false
python-versions = ">=3.9.0,<4.0"
@@ -3043,7 +3043,7 @@ files = []
develop = true
[package.dependencies]
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
langgraph-checkpoint = "^2.0.4"
langgraph-sdk = "^0.1.42"
@@ -3070,7 +3070,7 @@ url = "libs/checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
version = "2.0.7"
version = "2.0.8"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3106,7 +3106,7 @@ url = "libs/checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
version = "0.1.42"
version = "0.1.43"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3585,6 +3585,7 @@ optional = false
python-versions = ">=3.6"
files = [
{file = "mkdocs-redirects-1.2.1.tar.gz", hash = "sha256:9420066d70e2a6bb357adf86e67023dcdca1857f97f07c7fe450f8f1fb42f861"},
{file = "mkdocs_redirects-1.2.1-py3-none-any.whl", hash = "sha256:497089f9e0219e7389304cffefccdfa1cac5ff9509f2cb706f4c9b221726dffb"},
]
[package.dependencies]
@@ -5096,7 +5097,6 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
@@ -5107,7 +5107,6 @@ description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
]
@@ -6167,11 +6166,6 @@ files = [
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"},
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"},
{file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"},
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"},
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"},
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"},
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"},
{file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"},
{file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"},
@@ -6962,6 +6956,7 @@ description = "Automatically mock your HTTP interactions to simplify and speed u
optional = false
python-versions = ">=3.8"
files = [
{file = "vcrpy-6.0.1-py2.py3-none-any.whl", hash = "sha256:621c3fb2d6bd8aa9f87532c688e4575bcbbde0c0afeb5ebdb7e14cac409edfdd"},
{file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"},
]