Merge branch 'main' into vb/add-sync-client

This commit is contained in:
Vadym Barda
2024-09-23 17:39:54 -04:00
committed by GitHub
6 changed files with 141 additions and 59 deletions
+92 -51
View File
@@ -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 users 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 @@
"<div class=\"admonition note\">\n",
" <p class=\"admonition-title\">Typing shared state keys</p>\n",
" <p style=\"margin-top: 5px;\">\n",
" Shared state channels (keys) MUST be dictionaries (see <code>info</code> channel in the AgentState example below)\n",
" Shared state channels (keys) MUST be dictionaries (see <code>info</code> channel in the State example below)\n",
" </p>\n",
"</div>"
]
},
{
"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",
"<info>\n",
"{info}\n",
"</info>\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,
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+18 -5
View File
@@ -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:
+1 -1
View File
@@ -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"
+28
View File
@@ -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}"