diff --git a/docs/docs/how-tos/memory/shared-state.ipynb b/docs/docs/how-tos/memory/shared-state.ipynb
index 979e0497b..83f18484c 100644
--- a/docs/docs/how-tos/memory/shared-state.ipynb
+++ b/docs/docs/how-tos/memory/shared-state.ipynb
@@ -7,10 +7,11 @@
"source": [
"# How to share state between threads\n",
"\n",
- "By default, state in a graph is scoped to that thread.\n",
- "LangGraph also allows you to specify a \"scope\" for a given key/value pair that exists between threads. This can be useful for storing information that is shared between threads. For instance, you may want to store information about a user's preferences expressed in one thread, and then use that information in another thread.\n",
+ "By default, state is scoped to a single thread. LangGraph also lets you customize the scope for a given key-value pair. You can use this to share information between threads.\n",
"\n",
- "In this notebook we will go through an example of how to construct and use such a graph.\n",
+ "For instance, you can persist each user’s preferences to shared state and reuse them in new conversational threads.\n",
+ "\n",
+ "In this notebook, we will show how to construct and use such a graph.\n",
"\n",
"## Setup\n",
"\n",
@@ -19,7 +20,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"id": "3457aadf",
"metadata": {},
"outputs": [],
@@ -30,7 +31,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"id": "aa2c64a7",
"metadata": {},
"outputs": [],
@@ -73,44 +74,44 @@
"
\n",
"
Typing shared state keys
\n",
"
\n",
- " Shared state channels (keys) MUST be dictionaries (see info channel in the AgentState example below)\n",
+ " Shared state channels (keys) MUST be dictionaries (see info channel in the State example below)\n",
"
\n",
"
"
]
},
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": 5,
"id": "a7f303d6-612e-4e34-bf36-29d4ed25d802",
"metadata": {},
"outputs": [],
"source": [
- "from langgraph.graph.graph import START, END\n",
"from langgraph.graph.message import MessagesState\n",
"from langgraph.graph.state import StateGraph\n",
"from langgraph.store.memory import MemoryStore\n",
"from langgraph.managed.shared_value import SharedValue\n",
- "from typing import TypedDict, Annotated, Any\n",
+ "from typing import Literal, TypedDict, Annotated\n",
"import uuid\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
- "class AgentState(MessagesState):\n",
+ "class State(MessagesState):\n",
" # We use an info key to track information\n",
" # This is scoped to a user_id, so it will be information specific to each user\n",
- " info: Annotated[dict, SharedValue.on(\"user_id\")]\n",
+ " info: Annotated[dict[str, dict], SharedValue.on(\"user_id\")]\n",
"\n",
"\n",
"# We will give this as a tool to the agent\n",
"# This will let the agent call this tool to save a fact\n",
"class Info(TypedDict):\n",
" \"\"\"This tool should be called when you want to save a new fact about the user.\n",
- " \n",
+ "\n",
" Attributes:\n",
" fact (str): A fact about the user.\n",
" topic (str): The topic related the fact is about, i.e. Food, Location, Movies, etc.\n",
" \"\"\"\n",
+ "\n",
" fact: str\n",
" topic: str\n",
"\n",
@@ -118,17 +119,19 @@
"# This is the prompt we give the agent\n",
"# We will pass known info into the prompt\n",
"# We will tell it to use the Info tool to save more\n",
- "prompt = \"\"\"You are helpful assistant.\n",
- "\n",
- "Here is what you know about the user:\n",
+ "prompt = \"\"\"You are a helpful assistant that learns about users to provide better assistance.\n",
"\n",
+ "Current user information:\n",
"\n",
"{info}\n",
"\n",
"\n",
- "Help out the user. If the user tells you any information about themselves, save the information using the `Info` tool.\n",
+ "Instructions:\n",
+ "1. Use the `Info` tool to save new information the user shares.\n",
+ "2. Save facts, opinions, preferences, and experiences.\n",
+ "3. Your goal: Improve assistance by building a user profile over time.\n",
"\n",
- "This means if the user provides any sort of fact about themselves, be it an opinion they have, a fact about themselves, etc. SAVE IT!\n",
+ "Remember: Every piece of information helps you serve the user better in future interactions.\n",
"\"\"\"\n",
"\n",
"\n",
@@ -136,39 +139,45 @@
"model = ChatOpenAI().bind_tools([Info])\n",
"\n",
"\n",
- "# Our first node - this will call the model\n",
- "def call_model(state):\n",
- " # We get all facts and assemble them into a string\n",
- " facts = [d['fact'] for d in state['info'].values()]\n",
- " info = \"\\n\".join(facts)\n",
+ "def call_model(state: State):\n",
+ " \"\"\"Call the model.\"\"\"\n",
+ " # The info value here is scoped to the user_id\n",
+ " info = \"\\n\".join([d[\"fact\"] for d in state[\"info\"].values()])\n",
" # Format system prompt\n",
" system_msg = prompt.format(info=info)\n",
" # Call model\n",
- " response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + state['messages'])\n",
+ " response = model.invoke(\n",
+ " [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
+ " )\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Routing function to decide what to do next\n",
"# If no tool calls, then we end\n",
"# If tool calls, then we update memory\n",
- "def route(state):\n",
- " if len(state['messages'][-1].tool_calls) == 0:\n",
- " return END\n",
+ "def route(state) -> Literal[\"__end__\", \"update_memory\"]:\n",
+ " if len(state[\"messages\"][-1].tool_calls) == 0:\n",
+ " return \"__end__\"\n",
" else:\n",
" return \"update_memory\"\n",
"\n",
"\n",
- "# This function is responsible for updating the memory\n",
- "def update_memory(state):\n",
+ "def update_memory(state: State):\n",
+ " \"\"\"Update the memory.\"\"\"\n",
" tool_calls = []\n",
" memories = {}\n",
" # Each tool call is a new memory to save\n",
- " for tc in state['messages'][-1].tool_calls:\n",
+ " for tc in state[\"messages\"][-1].tool_calls:\n",
" # We append ToolMessages (to pass back to the LLM)\n",
" # This is needed because OpenAI requires each tool call be followed by a ToolMessage\n",
- " tool_calls.append({\"role\": \"tool\", \"content\": \"Saved!\", \"tool_call_id\": tc['id']})\n",
+ " tool_calls.append(\n",
+ " {\"role\": \"tool\", \"content\": \"Saved!\", \"tool_call_id\": tc[\"id\"]}\n",
+ " )\n",
" # We create a new memory from this tool call\n",
- " memories[str(uuid.uuid4())] = {\"fact\": tc['args']['fact'], \"topic\": tc['args']['topic']}\n",
+ " memories[str(uuid.uuid4())] = {\n",
+ " \"fact\": tc[\"args\"][\"fact\"],\n",
+ " \"topic\": tc[\"args\"][\"topic\"],\n",
+ " }\n",
" # Return the messages and memories to update the state with\n",
" return {\"messages\": tool_calls, \"info\": memories}\n",
"\n",
@@ -182,12 +191,12 @@
"kv = MemoryStore()\n",
"\n",
"# Construct this relatively simple graph\n",
- "graph = StateGraph(AgentState)\n",
+ "graph = StateGraph(State)\n",
"graph.add_node(call_model)\n",
"graph.add_node(update_memory)\n",
- "graph.add_edge(\"update_memory\", END)\n",
- "graph.add_edge(START, \"call_model\")\n",
- "graph.add_conditional_edges(\"call_model\", route)\n",
+ "graph.add_edge(\"update_memory\", \"__end__\")\n",
+ "graph.add_edge(\"__start__\", \"call_model\")\n",
+ "graph.add_conditional_edges(\"call_model\", route, [\"__end__\", \"update_memory\"])\n",
"graph = graph.compile(checkpointer=memory, store=kv)"
]
},
@@ -203,7 +212,7 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 6,
"id": "18bd8679-3a73-4033-bfb4-5093ac1f5d7f",
"metadata": {},
"outputs": [
@@ -211,11 +220,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "{'call_model': {'messages': [AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 171, 'total_tokens': 181}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-fbbb73a4-7c94-4db1-8761-44ea2fe9feaf-0', usage_metadata={'input_tokens': 171, 'output_tokens': 10, 'total_tokens': 181})]}}\n",
- "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_zMUXZfhOCFYvZg5TwXyBzw16', 'function': {'arguments': '{\"fact\":\"I like pepperoni pizza\",\"topic\":\"Food\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 193, 'total_tokens': 214}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-7297f9fb-1d3e-480e-b125-ab269f648158-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'I like pepperoni pizza', 'topic': 'Food'}, 'id': 'call_zMUXZfhOCFYvZg5TwXyBzw16', 'type': 'tool_call'}], usage_metadata={'input_tokens': 193, 'output_tokens': 21, 'total_tokens': 214})]}}\n",
- "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_zMUXZfhOCFYvZg5TwXyBzw16'}]}}\n",
- "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_GjshujJAeqoTuuBeHCD5YTPQ', 'function': {'arguments': '{\"fact\":\"I just moved to SF\",\"topic\":\"Location\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 239, 'total_tokens': 260}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4abea1d6-7ccb-49b4-b805-0e04ebb542e3-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'I just moved to SF', 'topic': 'Location'}, 'id': 'call_GjshujJAeqoTuuBeHCD5YTPQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 239, 'output_tokens': 21, 'total_tokens': 260})]}}\n",
- "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_GjshujJAeqoTuuBeHCD5YTPQ'}]}}\n"
+ "{'call_model': {'messages': [AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 181, 'total_tokens': 191, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-865472b7-68e0-4b93-bf63-13bd1dc4f3f0-0', usage_metadata={'input_tokens': 181, 'output_tokens': 10, 'total_tokens': 191})]}}\n",
+ "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_BcSTNM6xueW6lgcaA8GdBuy4', 'function': {'arguments': '{\"fact\":\"likes pepperoni pizza\",\"topic\":\"Food\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 203, 'total_tokens': 223, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-8adf64eb-db04-4232-99ce-9555ac7a9146-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_BcSTNM6xueW6lgcaA8GdBuy4', 'type': 'tool_call'}], usage_metadata={'input_tokens': 203, 'output_tokens': 20, 'total_tokens': 223})]}}\n",
+ "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_BcSTNM6xueW6lgcaA8GdBuy4'}]}}\n",
+ "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_eN6R6i9jLvLNpxvXVU4A3y08', 'function': {'arguments': '{\"fact\":\"just moved to San Francisco\",\"topic\":\"Location\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 247, 'total_tokens': 268, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-8ab84d8c-1a21-4b07-940c-74f6248ce6ea-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'just moved to San Francisco', 'topic': 'Location'}, 'id': 'call_eN6R6i9jLvLNpxvXVU4A3y08', 'type': 'tool_call'}], usage_metadata={'input_tokens': 247, 'output_tokens': 21, 'total_tokens': 268})]}}\n",
+ "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_eN6R6i9jLvLNpxvXVU4A3y08'}]}}\n"
]
}
],
@@ -223,15 +232,25 @@
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
"\n",
"# First let's just say hi to the AI\n",
- "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"hi\"}]}, config, stream_mode=\"updates\"):\n",
+ "for update in graph.stream(\n",
+ " {\"messages\": [{\"role\": \"user\", \"content\": \"hi\"}]}, config, stream_mode=\"updates\"\n",
+ "):\n",
" print(update)\n",
"\n",
"# Let's continue the conversation (by passing the same config) and tell the AI we like pepperoni pizza\n",
- "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"i like pepperoni pizza\"}]}, config, stream_mode=\"updates\"):\n",
+ "for update in graph.stream(\n",
+ " {\"messages\": [{\"role\": \"user\", \"content\": \"i like pepperoni pizza\"}]},\n",
+ " config,\n",
+ " stream_mode=\"updates\",\n",
+ "):\n",
" print(update)\n",
"\n",
"# Let's continue the conversation even further (by passing the same config) and tell the AI we live in SF\n",
- "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"i also just moved to SF\"}]}, config, stream_mode=\"updates\"):\n",
+ "for update in graph.stream(\n",
+ " {\"messages\": [{\"role\": \"user\", \"content\": \"i also just moved to SF\"}]},\n",
+ " config,\n",
+ " stream_mode=\"updates\",\n",
+ "):\n",
" print(update)"
]
},
@@ -247,7 +266,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 7,
"id": "e240f025-ff8b-4d17-beb7-2420c0575dd9",
"metadata": {},
"outputs": [
@@ -255,14 +274,25 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "{'call_model': {'messages': [AIMessage(content=\"Sure! Since you just moved to San Francisco, how about trying some popular local spots? Here are a few restaurant recommendations in SF:\\n\\n1. Tony's Pizza Napoletana - Known for their delicious pepperoni pizza!\\n2. The Slanted Door - A popular Vietnamese restaurant in the city.\\n3. Zuni Cafe - A classic American restaurant with a great ambiance.\\n4. Tartine Bakery - Perfect for a casual dinner with amazing baked goods.\\n5. State Bird Provisions - A unique dining experience with small plates and a lively atmosphere.\\n\\nFeel free to explore these options and enjoy your dinner! If you need more recommendations or information about a specific cuisine, let me know!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 138, 'prompt_tokens': 197, 'total_tokens': 335}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-de8ad08c-0810-4bb5-b2e8-d3dc89522f8e-0', usage_metadata={'input_tokens': 197, 'output_tokens': 138, 'total_tokens': 335})]}}\n"
+ "{'call_model': {'messages': [AIMessage(content=\"I can help with that! Since you just moved to San Francisco, how about trying some local favorites? Here are a few restaurants you might enjoy:\\n\\n1. Tony's Pizza Napoletana - Known for their delicious pepperoni pizza.\\n2. The House - Offers a mix of Asian fusion dishes.\\n3. Tadich Grill - A historic seafood restaurant with a cozy atmosphere.\\n4. Zuni Cafe - Famous for its roast chicken and innovative cuisine.\\n5. La Taqueria - A popular spot for authentic Mexican tacos.\\n\\nFeel free to explore these options and let me know if you'd like more recommendations or information about any specific cuisine!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 131, 'prompt_tokens': 206, 'total_tokens': 337, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-8530de74-bc07-4b31-b58d-4f4c064918b6-0', usage_metadata={'input_tokens': 206, 'output_tokens': 131, 'total_tokens': 337})]}}\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
"\n",
- "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\"}]}, config, stream_mode=\"updates\"):\n",
+ "for update in graph.stream(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\",\n",
+ " }\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " stream_mode=\"updates\",\n",
+ "):\n",
" print(update)"
]
},
@@ -280,7 +310,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 8,
"id": "f9bf2c15",
"metadata": {},
"outputs": [
@@ -288,14 +318,25 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "{'call_model': {'messages': [AIMessage(content='I can definitely help you with that! To provide you with personalized restaurant recommendations, could you please let me know your location or any specific preferences you have for dinner?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 185, 'total_tokens': 219}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-5a483acf-1289-4d7f-b707-97760a8c3620-0', usage_metadata={'input_tokens': 185, 'output_tokens': 34, 'total_tokens': 219})]}}\n"
+ "{'call_model': {'messages': [AIMessage(content='I can help you with that! Could you please provide me with your location or a preferred cuisine for dinner?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 195, 'total_tokens': 218, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-3f7eaa92-d3f0-4cca-ab13-0ecd7b922b9a-0', usage_metadata={'input_tokens': 195, 'output_tokens': 23, 'total_tokens': 218})]}}\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
"\n",
- "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\"}]}, config, stream_mode=\"updates\"):\n",
+ "for update in graph.stream(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\",\n",
+ " }\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " stream_mode=\"updates\",\n",
+ "):\n",
" print(update)"
]
},
@@ -324,7 +365,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.9"
+ "version": "3.11.2"
}
},
"nbformat": 4,
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index 59592acd4..0466bff40 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
-version = "1.0.7"
+version = "1.0.8"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml
index f37bfc72d..f050abb8d 100644
--- a/libs/checkpoint-sqlite/pyproject.toml
+++ b/libs/checkpoint-sqlite/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
-version = "1.0.3"
+version = "1.0.4"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py
index 52b80f75a..be87c0f0f 100644
--- a/libs/langgraph/langgraph/prebuilt/tool_node.py
+++ b/libs/langgraph/langgraph/prebuilt/tool_node.py
@@ -43,9 +43,20 @@ INVALID_TOOL_NAME_ERROR_TEMPLATE = (
TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
-def str_output(output: Any) -> str:
+def msg_content_output(output: Any) -> str | List[dict]:
+ recognized_content_block_types = ("image", "image_url", "text", "json")
if isinstance(output, str):
return output
+ elif all(
+ [
+ isinstance(x, dict) and x.get("type") in recognized_content_block_types
+ for x in output
+ ]
+ ):
+ return output
+ # Technically a list of strings is also valid message content but it's not currently
+ # well tested that all chat models support this. And for backwards compatibility
+ # we want to make sure we don't break any existing ToolNode usage.
else:
try:
return json.dumps(output, ensure_ascii=False)
@@ -138,8 +149,9 @@ class ToolNode(RunnableCallable):
tool_message: ToolMessage = self.tools_by_name[call["name"]].invoke(
input, config
)
- # TODO: handle this properly in core
- tool_message.content = str_output(tool_message.content)
+ tool_message.content = cast(
+ Union[str, list], msg_content_output(tool_message.content)
+ )
return tool_message
except Exception as e:
if not self.handle_tool_errors:
@@ -155,8 +167,9 @@ class ToolNode(RunnableCallable):
tool_message: ToolMessage = await self.tools_by_name[call["name"]].ainvoke(
input, config
)
- # TODO: handle this properly in core
- tool_message.content = str_output(tool_message.content)
+ tool_message.content = cast(
+ Union[str, list], msg_content_output(tool_message.content)
+ )
return tool_message
except Exception as e:
if not self.handle_tool_errors:
diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml
index f673688a5..815b35cd4 100644
--- a/libs/langgraph/pyproject.toml
+++ b/libs/langgraph/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
-version = "0.2.24"
+version = "0.2.25"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py
index 0fc1685e5..6e1cd081a 100644
--- a/libs/langgraph/tests/test_prebuilt.py
+++ b/libs/langgraph/tests/test_prebuilt.py
@@ -262,6 +262,12 @@ async def test_tool_node():
{"key_1": some_other_val, "key_2": "baz"},
]
+ async def tool4(some_val: int, some_other_val: str) -> str:
+ """Tool 4 docstring."""
+ return [
+ {"type": "image_url", "image_url": {"url": "abdc"}},
+ ]
+
result = ToolNode([tool1]).invoke(
{
"messages": [
@@ -397,6 +403,28 @@ async def test_tool_node():
)
assert tool_message.tool_call_id == "some 0"
+ # list of content blocks tool content
+ result4 = await ToolNode([tool4]).ainvoke(
+ {
+ "messages": [
+ AIMessage(
+ "hi?",
+ tool_calls=[
+ {
+ "name": "tool4",
+ "args": {"some_val": 2, "some_other_val": "bar"},
+ "id": "some 0",
+ }
+ ],
+ )
+ ]
+ }
+ )
+ tool_message: ToolMessage = result4["messages"][-1]
+ assert tool_message.type == "tool"
+ assert tool_message.content == [{"type": "image_url", "image_url": {"url": "abdc"}}]
+ assert tool_message.tool_call_id == "some 0"
+
def my_function(some_val: int, some_other_val: str) -> str:
return f"{some_val} - {some_other_val}"