Compare commits

..
17 Commits
Author SHA1 Message Date
Vadym BardaandGitHub fad4de789f sdk-py: release 0.1.26 (#1026) 2024-07-15 13:44:41 -04:00
Vadym BardaandGitHub bf10f39b6b docs: update delete messages how-to for clarity (#1025) 2024-07-15 13:36:19 -04:00
Vadym BardaandGitHub ebf6316fa7 docs: fix collapsible code block affecting TOC in tutorial (#1024) 2024-07-15 13:16:28 -04:00
Vadym BardaandGitHub 647c5737f0 sdk-py: add if_exists to threads.create (#1009) 2024-07-15 12:15:43 -04:00
Vedanta SPandGitHub 9e7695085e Update langgraph_self_rag_local.ipynb (#1014)
replaced pprint with print
2024-07-14 16:32:07 -04:00
Nuno CamposandGitHub 8cf10fa460 Specific error when updating state as an unknown node (#1015) 2024-07-14 06:54:01 -07:00
Vadym BardaandGitHub 3ef65c37b3 docs: add note for langgraph cloud checkpointers (#1011) 2024-07-12 16:48:03 -04:00
Vadym BardaandGitHub 4b5152fde5 docs: small fix in postgres docs (#1010) 2024-07-12 16:27:58 -04:00
Jacob LeeandGitHub 07c3c5ff6f Hide output spacer column in docs (#1008) 2024-07-12 13:00:01 -07:00
Lance MartinandGitHub ada17d2ccb Improve visuals (#1005) 2024-07-12 12:55:32 -07:00
738f725aea Support multiple interruptions after resuming execution (#1004)
* Support multiple interruptions

* Add async test, Use a different fix

* Update comment

---------

Co-authored-by: Nuno Campos <nuno@langchain.dev>
2024-07-12 12:53:17 -07:00
Jacob LeeandGitHub 558a513a1a Remove Jupyter cell 'In[<number>]' display column from docs (#1007) 2024-07-12 12:47:07 -07:00
Vadym BardaandGitHub 5a55974f15 docs: update postgres checkpointer (#1001) 2024-07-12 13:40:55 -04:00
Vadym BardaandGitHub f84f9374d3 docs: bring back how-to on passing runtime values to tools (#1003) 2024-07-12 11:52:34 -04:00
Vadym BardaandGitHub f8c87afee1 docs: update streaming from within the tool to use dispatch_custom_event (#1000) 2024-07-11 21:31:55 -04:00
dedbdefd93 [Docs] Added Asynchronous implementation of MongoDB persistence (#983)
---------

Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
Co-authored-by: Vadym Barda <vadym@langchain.dev>
2024-07-11 21:20:19 -04:00
Nuno Campos f13cf5dc2c Update snapshot tests after core release 2024-07-11 17:06:12 -07:00
21 changed files with 959 additions and 220 deletions
-1
View File
@@ -88,7 +88,6 @@ _HIDE = set(
"dynamically-returning-directly.ipynb",
"force-calling-a-tool-first.ipynb",
"managing-agent-steps.ipynb",
"pass-run-time-values-to-tools.ipynb",
"respond-in-format.ipynb",
"quickstart.ipynb",
"human-in-the-loop.ipynb",
+1
View File
@@ -58,6 +58,7 @@ These guides show how to use different streaming modes.
- [How to run graph asynchronously](async.ipynb)
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
- [How to use a Pydantic model as your state](state-model.ipynb)
- [How to use a context object in state](state-context-key.ipynb)
+1
View File
@@ -156,6 +156,7 @@ nav:
- Run graph asynchronously: how-tos/async.ipynb
- Visualize your graph: how-tos/visualization.ipynb
- Add runtime configuration: how-tos/configuration.ipynb
- Pass runtime values to tools: how-tos/pass-run-time-values-to-tools.ipynb
- Use Pydantic model as state: how-tos/state-model.ipynb
- Use a context object in state: how-tos/state-context-key.ipynb
- Prebuilt ReAct Agent:
+7
View File
@@ -146,6 +146,13 @@
display: none !important;
}
.jupyter-wrapper .jp-CodeCell .jp-Cell-inputWrapper .jp-InputPrompt.jp-InputArea-prompt {
display: none !important;
}
.jupyter-wrapper .jp-Notebook .jp-Cell .jp-OutputPrompt {
display: none !important;
}
</style>
{% endblock %}
+2 -1
View File
@@ -1224,7 +1224,8 @@
"graph = graph_builder.compile(checkpointer=memory)\n",
"```\n",
"</pre>\n",
"</pre>details"
"</pre>\n",
"</details>"
]
},
{
File diff suppressed because one or more lines are too long
+52 -46
View File
@@ -28,7 +28,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
@@ -50,7 +50,15 @@
"execution_count": 2,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
@@ -94,7 +102,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 4,
"id": "378899a9-3b9a-4748-95b6-eb00e0828677",
"metadata": {},
"outputs": [],
@@ -176,7 +184,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 5,
"id": "57b27553-21be-43e5-ac48-d1d0a3aa0dca",
"metadata": {},
"outputs": [
@@ -189,7 +197,7 @@
"hi! I'm bob\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Hello Bob! It's nice to meet you. How can I assist you today?\n",
"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I assist you today?\n",
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"what's my name?\n",
@@ -225,20 +233,20 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 6,
"id": "8a850529-d038-48f7-b5a2-8d4d2923f83a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[HumanMessage(content=\"hi! I'm bob\", id='3e1098f8-2657-42d3-b58a-7c2f46930b8c'),\n",
" AIMessage(content=\"Hello Bob! It's nice to meet you. How can I assist you today?\", response_metadata={'id': 'msg_01HT8MUEN4p16wbYv9Xm7kfr', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 20}}, id='run-86348912-72c4-42b0-b3e0-a47c4ebd1e52-0'),\n",
" HumanMessage(content=\"what's my name?\", id='9c3ef235-ec5c-4e57-a3b2-c17502de496d'),\n",
" AIMessage(content='Your name is Bob, as you introduced yourself at the beginning of our conversation.', response_metadata={'id': 'msg_01LVhb56f6RpAAoxASZrLzmK', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 40, 'output_tokens': 19}}, id='run-e3d7447f-046a-4dfa-8813-38134dbcd1ef-0')]"
"[HumanMessage(content=\"hi! I'm bob\", id='bc1c6dd2-3bb9-4aa9-b7af-3c6af7e173ea'),\n",
" AIMessage(content=\"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I assist you today?\", response_metadata={'id': 'msg_01XPSAenmSqK8rX2WgPZHfz7', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 32}}, id='run-1c69af09-adb1-412d-9010-2456e5a555fb-0', usage_metadata={'input_tokens': 12, 'output_tokens': 32, 'total_tokens': 44}),\n",
" HumanMessage(content=\"what's my name?\", id='f3c71afe-8ce2-4ed0-991e-65021f03b0a5'),\n",
" AIMessage(content='Your name is Bob, as you introduced yourself at the beginning of our conversation.', response_metadata={'id': 'msg_01BPZdwsjuMAbC1YAkqawXaF', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 52, 'output_tokens': 19}}, id='run-b2eb9137-2f4e-446f-95f5-3d5f621a2cf8-0', usage_metadata={'input_tokens': 52, 'output_tokens': 19, 'total_tokens': 71})]"
]
},
"execution_count": 7,
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -258,18 +266,26 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 7,
"id": "df1a0970-7e64-4170-beef-2855d10eef42",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The class `RemoveMessage` is in beta. It is actively being worked on, so the API may change.\n",
" warn_beta(\n"
]
},
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '2',\n",
" 'thread_ts': '1ef3d750-5bc4-67c6-8005-9490a1b276f5'}}"
" 'thread_ts': '1ef42d00-d9ad-6f24-8005-feb089654def'}}"
]
},
"execution_count": 9,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
@@ -289,19 +305,19 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 8,
"id": "8bfe4ffa-e170-43bc-aec4-6e36ac620931",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[AIMessage(content=\"Hello Bob! It's nice to meet you. How can I assist you today?\", response_metadata={'id': 'msg_01HT8MUEN4p16wbYv9Xm7kfr', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 20}}, id='run-86348912-72c4-42b0-b3e0-a47c4ebd1e52-0'),\n",
" HumanMessage(content=\"what's my name?\", id='9c3ef235-ec5c-4e57-a3b2-c17502de496d'),\n",
" AIMessage(content='Your name is Bob, as you introduced yourself at the beginning of our conversation.', response_metadata={'id': 'msg_01LVhb56f6RpAAoxASZrLzmK', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 40, 'output_tokens': 19}}, id='run-e3d7447f-046a-4dfa-8813-38134dbcd1ef-0')]"
"[AIMessage(content=\"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I assist you today?\", response_metadata={'id': 'msg_01XPSAenmSqK8rX2WgPZHfz7', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 32}}, id='run-1c69af09-adb1-412d-9010-2456e5a555fb-0', usage_metadata={'input_tokens': 12, 'output_tokens': 32, 'total_tokens': 44}),\n",
" HumanMessage(content=\"what's my name?\", id='f3c71afe-8ce2-4ed0-991e-65021f03b0a5'),\n",
" AIMessage(content='Your name is Bob, as you introduced yourself at the beginning of our conversation.', response_metadata={'id': 'msg_01BPZdwsjuMAbC1YAkqawXaF', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 52, 'output_tokens': 19}}, id='run-b2eb9137-2f4e-446f-95f5-3d5f621a2cf8-0', usage_metadata={'input_tokens': 52, 'output_tokens': 19, 'total_tokens': 71})]"
]
},
"execution_count": 11,
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
@@ -323,7 +339,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 9,
"id": "bb22ede0-e153-4fd0-a4c0-f9af2f7663b1",
"metadata": {},
"outputs": [],
@@ -376,7 +392,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 10,
"id": "3975f34c-c243-40ea-b9d2-424d50a48dc9",
"metadata": {},
"outputs": [
@@ -384,21 +400,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"hi! I'm bob\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"It's nice to meet you, Bob! How can I assist you today?\n",
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"what's my name?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"You said your name is Bob.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"You said your name is Bob.\n"
"[('human', \"hi! I'm bob\")]\n",
"[('human', \"hi! I'm bob\"), ('ai', \"Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know how I can assist you.\")]\n",
"[('human', \"hi! I'm bob\"), ('ai', \"Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know how I can assist you.\"), ('human', \"what's my name?\")]\n",
"[('human', \"hi! I'm bob\"), ('ai', \"Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know how I can assist you.\"), ('human', \"what's my name?\"), ('ai', 'You said your name is Bob, so that is the name I have for you.')]\n",
"[('ai', \"Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know how I can assist you.\"), ('human', \"what's my name?\"), ('ai', 'You said your name is Bob, so that is the name I have for you.')]\n"
]
}
],
@@ -408,12 +414,12 @@
"config = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"input_message = HumanMessage(content=\"hi! I'm bob\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()\n",
" print([(message.type, message.content) for message in event[\"messages\"]])\n",
"\n",
"\n",
"input_message = HumanMessage(content=\"what's my name?\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
" print([(message.type, message.content) for message in event[\"messages\"]])"
]
},
{
@@ -426,19 +432,19 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 11,
"id": "a3e15abb-81d8-4072-9f10-61ae0fd61dac",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[AIMessage(content=\"It's nice to meet you, Bob! How can I assist you today?\", response_metadata={'id': 'msg_01QMoxepDiCcKQ6XFgge1QQT', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 19}}, id='run-de13ba05-095d-4fd1-907a-6766ef3bf57b-0'),\n",
" HumanMessage(content=\"what's my name?\", id='8292e725-8fc4-487e-a9b6-75f8b136bec2'),\n",
" AIMessage(content='You said your name is Bob.', response_metadata={'id': 'msg_01DfWfaxavdMCqtoQRmC3mc4', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 39, 'output_tokens': 10}}, id='run-28167c82-e126-47e4-854c-623e50c8af22-0')]"
"[AIMessage(content=\"Hello Bob! It's nice to meet you. I'm an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know how I can assist you.\", response_metadata={'id': 'msg_01XPEgPPbcnz5BbGWUDWTmzG', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 48}}, id='run-eded3820-b6a9-4d66-9210-03ca41787ce6-0', usage_metadata={'input_tokens': 12, 'output_tokens': 48, 'total_tokens': 60}),\n",
" HumanMessage(content=\"what's my name?\", id='a0ea2097-3280-402b-92e1-67177b807ae8'),\n",
" AIMessage(content='You said your name is Bob, so that is the name I have for you.', response_metadata={'id': 'msg_01JGT62pxhrhN4SykZ57CSjW', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 68, 'output_tokens': 20}}, id='run-ace3519c-81f8-45fe-a777-91f42d48b3a3-0', usage_metadata={'input_tokens': 68, 'output_tokens': 20, 'total_tokens': 88})]"
]
},
"execution_count": 17,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
@@ -467,9 +473,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "langgraph",
"language": "python",
"name": "python3"
"name": "langgraph"
},
"language_info": {
"codemirror_mode": {
@@ -481,7 +487,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
+178 -27
View File
@@ -38,14 +38,6 @@
"</div> "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c3fde0a",
"metadata": {},
"outputs": [],
"source": [""]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
@@ -62,7 +54,10 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
@@ -78,7 +73,18 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -94,7 +100,10 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -112,7 +121,22 @@
"id": "14619607",
"metadata": {},
"outputs": [],
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# Add messages essentially does this with more\n",
"# robust handling\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
},
{
"cell_type": "markdown",
@@ -132,7 +156,19 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"]
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" return [\"The answer to your question lies within.\"]\n",
"\n",
"\n",
"tools = [search]"
]
},
{
"cell_type": "markdown",
@@ -149,7 +185,11 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
},
{
"cell_type": "markdown",
@@ -179,7 +219,13 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"]
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"# We will set streaming=True so that we can stream tokens\n",
"# See the streaming section for more information on this.\n",
"model = ChatOpenAI(temperature=0, streaming=True)"
]
},
{
"cell_type": "markdown",
@@ -197,7 +243,9 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": ["bound_model = model.bind_tools(tools)"]
"source": [
"bound_model = model.bind_tools(tools)"
]
},
{
"cell_type": "markdown",
@@ -232,7 +280,27 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": ["# Define the function that determines whether to continue or not\nfrom typing import Literal\n\n\ndef should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n \"\"\"Return the next node to execute.\"\"\"\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"__end__\"\n # Otherwise if there is, we continue\n return \"action\"\n\n\n# Define the function that calls the model\ndef call_model(state: State):\n response = model.invoke(state[\"messages\"])\n # We return a list, because this will get added to the existing list\n return {\"messages\": response}"]
"source": [
"# Define the function that determines whether to continue or not\n",
"from typing import Literal\n",
"\n",
"\n",
"def should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n",
" \"\"\"Return the next node to execute.\"\"\"\n",
" last_message = state[\"messages\"][-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"__end__\"\n",
" # Otherwise if there is, we continue\n",
" return \"action\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state: State):\n",
" response = model.invoke(state[\"messages\"])\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": response}"
]
},
{
"cell_type": "markdown",
@@ -248,7 +316,33 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
"source": [
"from langgraph.graph import StateGraph, START\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
},
{
"cell_type": "markdown",
@@ -266,7 +360,11 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
]
},
{
"cell_type": "code",
@@ -274,7 +372,25 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"]
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory)"
]
},
{
"cell_type": "markdown",
"id": "7654ebcc-2179-41b4-92d1-6666f6f8634f",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Note</p>\n",
" <p>\n",
" If you're using LangGraph Cloud, you <strong>don't need</strong> to pass checkpointer when compiling the graph, since it's done automatically.\n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "code",
@@ -293,7 +409,15 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(app.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
},
{
"cell_type": "markdown",
@@ -324,7 +448,14 @@
]
}
],
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"hi! I'm bob\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -345,7 +476,11 @@
]
}
],
"source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"input_message = HumanMessage(content=\"what is my name?\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -374,7 +509,15 @@
]
}
],
"source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"3\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"input_message = HumanMessage(content=\"what is my name?\")\n",
"for event in app.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"3\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -403,7 +546,15 @@
]
}
],
"source": ["input_message = HumanMessage(content=\"You forgot??\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"2\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"input_message = HumanMessage(content=\"You forgot??\")\n",
"for event in app.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -411,7 +562,7 @@
"id": "eb20430f",
"metadata": {},
"outputs": [],
"source": [""]
"source": []
}
],
"metadata": {
@@ -430,7 +581,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
"version": "3.11.9"
}
},
"nbformat": 4,
+405 -1
View File
@@ -278,7 +278,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
@@ -574,6 +574,410 @@
"\n",
"#The checkpoints from both the examples have been saved in the database."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Asynchronous implementation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#Async package for MongoDB\n",
"%pip install motor"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import pickle\n",
"from contextlib import AbstractContextManager\n",
"from types import TracebackType\n",
"from typing import Any, Dict,Optional,AsyncIterator\n",
"\n",
"from langchain_core.runnables import RunnableConfig\n",
"from typing_extensions import Self\n",
"\n",
"from langgraph.checkpoint.base import (\n",
" BaseCheckpointSaver,\n",
" Checkpoint,\n",
" CheckpointMetadata,\n",
" CheckpointTuple,\n",
" SerializerProtocol,\n",
")\n",
"from langgraph.serde.jsonplus import JsonPlusSerializer\n",
"from motor.motor_asyncio import AsyncIOMotorClient\n",
"\n",
"class JsonPlusSerializerCompat(JsonPlusSerializer):\n",
" \"\"\"A serializer that supports loading pickled checkpoints for backwards compatibility.\n",
"\n",
" This serializer extends the JsonPlusSerializer and adds support for loading pickled\n",
" checkpoints. If the input data starts with b\"\\x80\" and ends with b\".\", it is treated\n",
" as a pickled checkpoint and loaded using pickle.loads(). Otherwise, the default\n",
" JsonPlusSerializer behavior is used.\n",
"\n",
" Examples:\n",
" >>> import pickle\n",
" >>> from langgraph.checkpoint.sqlite import JsonPlusSerializerCompat\n",
" >>>\n",
" >>> serializer = JsonPlusSerializerCompat()\n",
" >>> pickled_data = pickle.dumps({\"key\": \"value\"})\n",
" >>> loaded_data = serializer.loads(pickled_data)\n",
" >>> print(loaded_data) # Output: {\"key\": \"value\"}\n",
" >>>\n",
" >>> json_data = '{\"key\": \"value\"}'.encode(\"utf-8\")\n",
" >>> loaded_data = serializer.loads(json_data)\n",
" >>> print(loaded_data) # Output: {\"key\": \"value\"}\n",
" \"\"\"\n",
"\n",
" def loads(self, data: bytes) -> Any:\n",
" if data.startswith(b\"\\x80\") and data.endswith(b\".\"):\n",
" return pickle.loads(data)\n",
" return super().loads(data)\n",
"\n",
"class MongoDBSaver(AbstractContextManager, BaseCheckpointSaver):\n",
" \"\"\"A checkpoint saver that stores checkpoints in a MongoDB database.\n",
"\n",
" Args:\n",
" client (AsyncIOMotorClient): The Async MongoDB client.\n",
" db_name (str): The name of the database to use.\n",
" collection_name (str): The name of the collection to use.\n",
" serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to JsonPlusSerializerCompat.\n",
"\n",
" Examples:\n",
"\n",
" >>> from motor.motor_asyncio import AsyncIOMotorClient\n",
" >>> from langgraph.checkpoint.mongodb import MongoDBSaver\n",
" >>> from langgraph.graph import StateGraph\n",
" >>>\n",
" >>> builder = StateGraph(int)\n",
" >>> builder.add_node(\"add_one\", lambda x: x + 1)\n",
" >>> builder.set_entry_point(\"add_one\")\n",
" >>> builder.set_finish_point(\"add_one\")\n",
" >>> client = AsyncIOMotorClient(\"mongodb://localhost:27017/\")\n",
" >>> memory = MongoDBSaver(client, \"checkpoints\", \"checkpoints\")\n",
" >>> graph = builder.compile(checkpointer=memory)\n",
" >>> config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
" >>> result = graph.ainvoke(3, config)\n",
" \"\"\"\n",
"\n",
" serde = JsonPlusSerializerCompat()\n",
"\n",
" client: AsyncIOMotorClient\n",
" db_name: str\n",
" collection_name: str\n",
"\n",
" def __init__(\n",
" self,\n",
" client: AsyncIOMotorClient,\n",
" db_name: str,\n",
" collection_name: str,\n",
" *,\n",
" serde: Optional[SerializerProtocol] = None,\n",
" ) -> None:\n",
" super().__init__(serde=serde)\n",
" self.client = client\n",
" self.db_name = db_name\n",
" self.collection_name = collection_name\n",
" self.collection = client[db_name][collection_name]\n",
"\n",
" def __enter__(self) -> Self:\n",
" return self\n",
"\n",
" def __exit__(\n",
" self,\n",
" __exc_type: Optional[type[BaseException]],\n",
" __exc_value: Optional[BaseException],\n",
" __traceback: Optional[TracebackType],\n",
" ) -> Optional[bool]:\n",
" return True\n",
"\n",
" async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
" \"\"\"Get a checkpoint tuple from the database.\n",
"\n",
" This method retrieves a checkpoint tuple from the MongoDB database based on the\n",
" provided config. If the config contains a \"thread_ts\" key, the checkpoint with\n",
" the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint\n",
" for the given thread ID is retrieved.\n",
"\n",
" Args:\n",
" config (RunnableConfig): The config to use for retrieving the checkpoint.\n",
"\n",
" Returns:\n",
" Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.\n",
" \"\"\"\n",
" if config[\"configurable\"].get(\"thread_ts\"):\n",
" query = {\n",
" \"thread_id\": config[\"configurable\"][\"thread_id\"],\n",
" \"thread_ts\": config[\"configurable\"][\"thread_ts\"],\n",
" }\n",
" else:\n",
" query = {\"thread_id\": config[\"configurable\"][\"thread_id\"]}\n",
" result = self.collection.find(query).sort(\"thread_ts\", -1).limit(1)\n",
" async for doc in result:\n",
" return CheckpointTuple(\n",
" config,\n",
" self.serde.loads(doc[\"checkpoint\"]),\n",
" self.serde.loads(doc[\"metadata\"]),\n",
" (\n",
" {\n",
" \"configurable\": {\n",
" \"thread_id\": doc[\"thread_id\"],\n",
" \"thread_ts\": doc[\"parent_ts\"],\n",
" }\n",
" }\n",
" if doc.get(\"parent_ts\")\n",
" else None\n",
" ),\n",
" )\n",
"\n",
" async def alist(\n",
" self,\n",
" config: Optional[RunnableConfig],\n",
" *,\n",
" filter: Optional[Dict[str, Any]] = None,\n",
" before: Optional[RunnableConfig] = None,\n",
" limit: Optional[int] = None,\n",
" ) -> AsyncIterator[CheckpointTuple]:\n",
" \"\"\"List checkpoints from the database.\n",
"\n",
" This method retrieves a list of checkpoint tuples from the MongoDB database based\n",
" on the provided config. The checkpoints are ordered by timestamp in descending order.\n",
"\n",
" Args:\n",
" config (RunnableConfig): The config to use for listing the checkpoints.\n",
" before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None.\n",
" limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.\n",
"\n",
" Yields:\n",
" AsyncIterator[CheckpointTuple]: An Async iterator of checkpoint tuples.\n",
" \"\"\"\n",
" query = {}\n",
" if config is not None:\n",
" query[\"thread_id\"] = config[\"configurable\"][\"thread_id\"]\n",
" if filter:\n",
" for key, value in filter.items():\n",
" query[f\"metadata.{key}\"] = value\n",
" if before is not None:\n",
" query[\"thread_ts\"] = {\"$lt\": before[\"configurable\"][\"thread_ts\"]}\n",
" result = self.collection.find(query).sort(\"thread_ts\", -1).limit(limit)\n",
" if limit is not None:\n",
" result = result.limit(limit)\n",
" async for doc in result:\n",
" yield CheckpointTuple(\n",
" {\n",
" \"configurable\": {\n",
" \"thread_id\": doc[\"thread_id\"],\n",
" \"thread_ts\": doc[\"thread_ts\"],\n",
" }\n",
" },\n",
" self.serde.loads(doc[\"checkpoint\"]),\n",
" self.serde.loads(doc[\"metadata\"]),\n",
" (\n",
" {\n",
" \"configurable\": {\n",
" \"thread_id\": doc[\"thread_id\"],\n",
" \"thread_ts\": doc[\"parent_ts\"],\n",
" }\n",
" }\n",
" if doc.get(\"parent_ts\")\n",
" else None\n",
" ),\n",
" )\n",
"\n",
" async def aput(\n",
" self,\n",
" config: RunnableConfig,\n",
" checkpoint: Checkpoint,\n",
" metadata: CheckpointMetadata,\n",
" ) -> RunnableConfig:\n",
" \"\"\"Save a checkpoint to the database.\n",
"\n",
" This method saves a checkpoint to the MongoDB database. The checkpoint is associated\n",
" with the provided config and its parent config (if any).\n",
"\n",
" Args:\n",
" config (RunnableConfig): The config to associate with the checkpoint.\n",
" checkpoint (Checkpoint): The checkpoint to save.\n",
" metadata (Optional[dict[str, Any]]): Additional metadata to save with the checkpoint. Defaults to None.\n",
"\n",
" Returns:\n",
" RunnableConfig: The updated config containing the saved checkpoint's timestamp.\n",
" \"\"\"\n",
" doc = {\n",
" \"thread_id\": config[\"configurable\"][\"thread_id\"],\n",
" \"thread_ts\": checkpoint[\"id\"],\n",
" \"checkpoint\": self.serde.dumps(checkpoint),\n",
" \"metadata\": self.serde.dumps(metadata),\n",
" }\n",
" if config[\"configurable\"].get(\"thread_ts\"):\n",
" doc[\"parent_ts\"] = config[\"configurable\"][\"thread_ts\"]\n",
" await self.collection.insert_one(doc)\n",
" return {\n",
" \"configurable\": {\n",
" \"thread_id\": config[\"configurable\"][\"thread_id\"],\n",
" \"thread_ts\": checkpoint[\"id\"],\n",
" }\n",
" }"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example with basic graph"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph\n",
"checkpointer = MongoDBSaver(AsyncIOMotorClient(MONGO_URI), \"checkpoints_db\", \"checkpoints_collection\")\n",
"builder = StateGraph(int)\n",
"builder.add_node(\"add_one\", lambda x: x + 1)\n",
"builder.set_entry_point(\"add_one\")\n",
"builder.set_finish_point(\"add_one\")\n",
"graph = builder.compile(checkpointer=checkpointer)\n",
"config = {\"configurable\": {\"thread_id\": \"123\"}}\n",
"res = await graph.ainvoke(3,config)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"4"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"res"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'v': 1,\n",
" 'ts': '2024-07-10T11:34:28.485660+00:00',\n",
" 'id': '1ef3eb05-e0d1-651b-8004-15f129f5f4fb',\n",
" 'channel_values': {'__root__': 4, 'add_one': 'add_one'},\n",
" 'channel_versions': {'__start__': 5,\n",
" '__root__': 6,\n",
" 'start:add_one': 6,\n",
" 'add_one': 6},\n",
" 'versions_seen': {'__start__': {'__start__': 4},\n",
" 'add_one': {'start:add_one': 5}},\n",
" 'pending_sends': []}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await checkpointer.aget(config)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"CheckpointTuple(config={'configurable': {'thread_id': '123'}}, checkpoint={'v': 1, 'ts': '2024-07-10T11:34:28.485660+00:00', 'id': '1ef3eb05-e0d1-651b-8004-15f129f5f4fb', 'channel_values': {'__root__': 4, 'add_one': 'add_one'}, 'channel_versions': {'__start__': 5, '__root__': 6, 'start:add_one': 6, 'add_one': 6}, 'versions_seen': {'__start__': {'__start__': 4}, 'add_one': {'start:add_one': 5}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 4, 'writes': {'add_one': 4}}, parent_config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1'}})"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await checkpointer.aget_tuple(config)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CheckpointTuple(config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0d1-651b-8004-15f129f5f4fb'}}, checkpoint={'v': 1, 'ts': '2024-07-10T11:34:28.485660+00:00', 'id': '1ef3eb05-e0d1-651b-8004-15f129f5f4fb', 'channel_values': {'__root__': 4, 'add_one': 'add_one'}, 'channel_versions': {'__start__': 5, '__root__': 6, 'start:add_one': 6, 'add_one': 6}, 'versions_seen': {'__start__': {'__start__': 4}, 'add_one': {'start:add_one': 5}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 4, 'writes': {'add_one': 4}}, parent_config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1'}})\n",
"CheckpointTuple(config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1'}}, checkpoint={'v': 1, 'ts': '2024-07-10T11:34:28.477660+00:00', 'id': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1', 'channel_values': {'__root__': 3, 'start:add_one': '__start__'}, 'channel_versions': {'__start__': 5, '__root__': 5, 'start:add_one': 5, 'add_one': 4}, 'versions_seen': {'__start__': {'__start__': 4}, 'add_one': {'start:add_one': 2}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': None}, parent_config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0bb-659e-8002-de83b4764141'}})\n",
"CheckpointTuple(config={'configurable': {'thread_id': '123', 'thread_ts': '1ef3eb05-e0bb-659e-8002-de83b4764141'}}, checkpoint={'v': 1, 'ts': '2024-07-10T11:34:28.476662+00:00', 'id': '1ef3eb05-e0bb-659e-8002-de83b4764141', 'channel_values': {'__root__': 4, '__start__': 3}, 'channel_versions': {'__start__': 4, '__root__': 3, 'start:add_one': 3, 'add_one': 4}, 'versions_seen': {'__start__': {'__start__': 1}, 'add_one': {'start:add_one': 2}}, 'pending_sends': []}, metadata={'source': 'input', 'step': 2, 'writes': 3}, parent_config=None)\n"
]
}
],
"source": [
"list = checkpointer.alist(config, limit=3)\n",
"async for item in list:\n",
" print(item)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Checkpoints saved in MongoDB"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'_id': ObjectId('668e57930f55bbe62f358531'), 'thread_id': '123', 'thread_ts': '1ef3ea0c-18a5-67a6-bfff-0d85b77e4a09', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T09:42:43.453328+00:00\", \"id\": \"1ef3ea0c-18a5-67a6-bfff-0d85b77e4a09\", \"channel_values\": {\"__start__\": 3}, \"channel_versions\": {\"__start__\": 1}, \"versions_seen\": {}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"input\", \"step\": -1, \"writes\": 3}'}\n",
"{'_id': ObjectId('668e57930f55bbe62f358532'), 'thread_id': '123', 'thread_ts': '1ef3ea0c-18a7-6ea3-8000-9a52ba553d0c', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T09:42:43.454326+00:00\", \"id\": \"1ef3ea0c-18a7-6ea3-8000-9a52ba553d0c\", \"channel_values\": {\"__root__\": 3, \"start:add_one\": \"__start__\"}, \"channel_versions\": {\"__start__\": 2, \"__root__\": 2, \"start:add_one\": 2}, \"versions_seen\": {\"__start__\": {\"__start__\": 1}, \"add_one\": {}}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"loop\", \"step\": 0, \"writes\": null}', 'parent_ts': '1ef3ea0c-18a5-67a6-bfff-0d85b77e4a09'}\n",
"{'_id': ObjectId('668e57930f55bbe62f358533'), 'thread_id': '123', 'thread_ts': '1ef3ea0c-18bc-6b54-8001-ef5781939492', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T09:42:43.462843+00:00\", \"id\": \"1ef3ea0c-18bc-6b54-8001-ef5781939492\", \"channel_values\": {\"__root__\": 4, \"add_one\": \"add_one\"}, \"channel_versions\": {\"__start__\": 2, \"__root__\": 3, \"start:add_one\": 3, \"add_one\": 3}, \"versions_seen\": {\"__start__\": {\"__start__\": 1}, \"add_one\": {\"start:add_one\": 2}}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"loop\", \"step\": 1, \"writes\": {\"add_one\": 4}}', 'parent_ts': '1ef3ea0c-18a7-6ea3-8000-9a52ba553d0c'}\n",
"{'_id': ObjectId('668e71c4171972a41a226373'), 'thread_id': '123', 'thread_ts': '1ef3eb05-e0bb-659e-8002-de83b4764141', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T11:34:28.476662+00:00\", \"id\": \"1ef3eb05-e0bb-659e-8002-de83b4764141\", \"channel_values\": {\"__root__\": 4, \"__start__\": 3}, \"channel_versions\": {\"__start__\": 4, \"__root__\": 3, \"start:add_one\": 3, \"add_one\": 4}, \"versions_seen\": {\"__start__\": {\"__start__\": 1}, \"add_one\": {\"start:add_one\": 2}}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"input\", \"step\": 2, \"writes\": 3}'}\n",
"{'_id': ObjectId('668e71c4171972a41a226374'), 'thread_id': '123', 'thread_ts': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T11:34:28.477660+00:00\", \"id\": \"1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1\", \"channel_values\": {\"__root__\": 3, \"start:add_one\": \"__start__\"}, \"channel_versions\": {\"__start__\": 5, \"__root__\": 5, \"start:add_one\": 5, \"add_one\": 4}, \"versions_seen\": {\"__start__\": {\"__start__\": 4}, \"add_one\": {\"start:add_one\": 2}}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"loop\", \"step\": 3, \"writes\": null}', 'parent_ts': '1ef3eb05-e0bb-659e-8002-de83b4764141'}\n",
"{'_id': ObjectId('668e71c4171972a41a226375'), 'thread_id': '123', 'thread_ts': '1ef3eb05-e0d1-651b-8004-15f129f5f4fb', 'checkpoint': b'{\"v\": 1, \"ts\": \"2024-07-10T11:34:28.485660+00:00\", \"id\": \"1ef3eb05-e0d1-651b-8004-15f129f5f4fb\", \"channel_values\": {\"__root__\": 4, \"add_one\": \"add_one\"}, \"channel_versions\": {\"__start__\": 5, \"__root__\": 6, \"start:add_one\": 6, \"add_one\": 6}, \"versions_seen\": {\"__start__\": {\"__start__\": 4}, \"add_one\": {\"start:add_one\": 5}}, \"pending_sends\": []}', 'metadata': b'{\"source\": \"loop\", \"step\": 4, \"writes\": {\"add_one\": 4}}', 'parent_ts': '1ef3eb05-e0bd-6c9c-8003-aa9cb0fdedc1'}\n"
]
}
],
"source": [
"from pymongo import MongoClient\n",
"client = MongoClient(MONGO_URI)\n",
"database = client[\"checkpoints_db\"]\n",
"collection = database[\"checkpoints_collection\"]\n",
"\n",
"for doc in collection.find():\n",
" print(doc)"
]
}
],
"metadata": {
+165 -97
View File
@@ -51,6 +51,7 @@
" Union,\n",
" Tuple,\n",
" List,\n",
" Sequence\n",
")\n",
"\n",
"import psycopg\n",
@@ -174,6 +175,15 @@
" metadata BYTEA NOT NULL,\n",
" PRIMARY KEY (thread_id, thread_ts)\n",
" );\n",
" CREATE TABLE IF NOT EXISTS writes (\n",
" thread_id TEXT NOT NULL,\n",
" thread_ts TEXT NOT NULL,\n",
" task_id TEXT NOT NULL,\n",
" idx INTEGER NOT NULL,\n",
" channel TEXT NOT NULL,\n",
" value BYTEA,\n",
" PRIMARY KEY (thread_id, thread_ts, task_id, idx)\n",
" );\n",
" \"\"\"\n",
"\n",
" @staticmethod\n",
@@ -196,13 +206,13 @@
" def drop_tables(connection: psycopg.Connection, /) -> None:\n",
" \"\"\"Drop the table for the checkpoint saver.\"\"\"\n",
" with connection.cursor() as cur:\n",
" cur.execute(\"DROP TABLE IF EXISTS checkpoints;\")\n",
" cur.execute(\"DROP TABLE IF EXISTS checkpoints, writes;\")\n",
"\n",
" @staticmethod\n",
" async def adrop_tables(connection: psycopg.AsyncConnection, /) -> None:\n",
" \"\"\"Drop the table for the checkpoint saver.\"\"\"\n",
" async with connection.cursor() as cur:\n",
" await cur.execute(\"DROP TABLE IF EXISTS checkpoints;\")\n",
" await cur.execute(\"DROP TABLE IF EXISTS checkpoints, writes;\")\n",
"\n",
" UPSERT_CHECKPOINT_QUERY = \"\"\"\n",
" INSERT INTO checkpoints \n",
@@ -239,7 +249,7 @@
" self.UPSERT_CHECKPOINT_QUERY,\n",
" (\n",
" thread_id,\n",
" checkpoint[\"ts\"],\n",
" checkpoint[\"id\"],\n",
" parent_ts if parent_ts else None,\n",
" self.serde.dumps(checkpoint),\n",
" self.serde.dumps(metadata),\n",
@@ -249,7 +259,7 @@
" return {\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": checkpoint[\"ts\"],\n",
" \"thread_ts\": checkpoint[\"id\"],\n",
" },\n",
" }\n",
"\n",
@@ -278,7 +288,7 @@
" self.UPSERT_CHECKPOINT_QUERY,\n",
" (\n",
" thread_id,\n",
" checkpoint[\"ts\"],\n",
" checkpoint[\"id\"],\n",
" parent_ts if parent_ts else None,\n",
" self.serde.dumps(checkpoint),\n",
" self.serde.dumps(metadata),\n",
@@ -288,10 +298,68 @@
" return {\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": checkpoint[\"ts\"],\n",
" \"thread_ts\": checkpoint[\"id\"],\n",
" },\n",
" }\n",
"\n",
" UPSERT_WRITES_QUERY = \"\"\"\n",
" INSERT INTO writes\n",
" (thread_id, thread_ts, task_id, idx, channel, value)\n",
" VALUES\n",
" (%s, %s, %s, %s, %s, %s)\n",
" ON CONFLICT (thread_id, thread_ts, task_id, idx)\n",
" DO UPDATE SET value = EXCLUDED.value;\n",
" \"\"\"\n",
"\n",
" def put_writes(\n",
" self,\n",
" config: RunnableConfig,\n",
" writes: Sequence[Tuple[str, Any]],\n",
" task_id: str,\n",
" ) -> None:\n",
" with self._get_sync_connection() as conn:\n",
" with conn.cursor() as cur:\n",
" cur.executemany(\n",
" self.UPSERT_WRITES_QUERY,\n",
" [\n",
" (\n",
" str(config[\"configurable\"][\"thread_id\"]),\n",
" str(config[\"configurable\"][\"thread_ts\"]),\n",
" task_id,\n",
" idx,\n",
" channel,\n",
" self.serde.dumps(value),\n",
" )\n",
" for idx, (channel, value) in enumerate(writes)\n",
" ],\n",
" )\n",
" conn.commit()\n",
"\n",
" async def aput_writes(\n",
" self,\n",
" config: RunnableConfig,\n",
" writes: Sequence[Tuple[str, Any]],\n",
" task_id: str,\n",
" ) -> None:\n",
"\n",
" async with self._get_async_connection() as conn:\n",
" async with conn.cursor() as cur:\n",
" await cur.executemany(\n",
" self.UPSERT_WRITES_QUERY,\n",
" [\n",
" (\n",
" str(config[\"configurable\"][\"thread_id\"]),\n",
" str(config[\"configurable\"][\"thread_ts\"]),\n",
" task_id,\n",
" idx,\n",
" channel,\n",
" self.serde.dumps(value),\n",
" )\n",
" for idx, (channel, value) in enumerate(writes)\n",
" ],\n",
" )\n",
" await conn.commit()\n",
"\n",
" LIST_CHECKPOINTS_QUERY_STR = \"\"\"\n",
" SELECT checkpoint, metadata, thread_ts, parent_ts\n",
" FROM checkpoints\n",
@@ -404,6 +472,7 @@
" thread_ts = config[\"configurable\"].get(\"thread_ts\")\n",
" with self._get_sync_connection() as conn:\n",
" with conn.cursor() as cur:\n",
" # find the latest checkpoint for the thread_id\n",
" if thread_ts:\n",
" cur.execute(\n",
" self.GET_CHECKPOINT_BY_TS_QUERY,\n",
@@ -412,22 +481,6 @@
" \"thread_ts\": thread_ts,\n",
" },\n",
" )\n",
" value = cur.fetchone()\n",
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
" config=config,\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" }\n",
" if thread_ts\n",
" else None,\n",
" )\n",
" else:\n",
" cur.execute(\n",
" self.GET_CHECKPOINT_QUERY,\n",
@@ -435,28 +488,44 @@
" \"thread_id\": thread_id,\n",
" },\n",
" )\n",
" value = cur.fetchone()\n",
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
" config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" },\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": parent_ts,\n",
" }\n",
"\n",
" # if a checkpoint is found, return it\n",
" if value := cur.fetchone():\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" if not config[\"configurable\"].get(\"thread_ts\"):\n",
" config = {\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" if parent_ts\n",
" else None,\n",
" )\n",
" return None\n",
" }\n",
"\n",
" # find any pending writes\n",
" cur.execute(\n",
" \"SELECT task_id, channel, value FROM writes WHERE thread_id = %(thread_id)s AND thread_ts = %(thread_ts)s\",\n",
" {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" },\n",
" )\n",
" # deserialize the checkpoint and metadata\n",
" return CheckpointTuple(\n",
" config=config,\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": parent_ts,\n",
" }\n",
" }\n",
" if parent_ts\n",
" else None,\n",
" pending_writes=[\n",
" (task_id, channel, self.serde.loads(value))\n",
" for task_id, channel, value in cur\n",
" ]\n",
" )\n",
"\n",
" async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
" \"\"\"Get the checkpoint tuple for the given configuration.\n",
@@ -474,6 +543,7 @@
" thread_ts = config[\"configurable\"].get(\"thread_ts\")\n",
" async with self._get_async_connection() as conn:\n",
" async with conn.cursor() as cur:\n",
" # find the latest checkpoint for the thread_id\n",
" if thread_ts:\n",
" await cur.execute(\n",
" self.GET_CHECKPOINT_BY_TS_QUERY,\n",
@@ -482,22 +552,6 @@
" \"thread_ts\": thread_ts,\n",
" },\n",
" )\n",
" value = await cur.fetchone()\n",
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
" config=config,\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" }\n",
" if thread_ts\n",
" else None,\n",
" )\n",
" else:\n",
" await cur.execute(\n",
" self.GET_CHECKPOINT_QUERY,\n",
@@ -505,29 +559,43 @@
" \"thread_id\": thread_id,\n",
" },\n",
" )\n",
" value = await cur.fetchone()\n",
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
" config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" },\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": parent_ts,\n",
" }\n",
" # if a checkpoint is found, return it\n",
" if value := await cur.fetchone():\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" if not config[\"configurable\"].get(\"thread_ts\"):\n",
" config = {\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" }\n",
" if parent_ts\n",
" else None,\n",
" )\n",
" }\n",
"\n",
" return None\n",
" # find any pending writes\n",
" await cur.execute(\n",
" \"SELECT task_id, channel, value FROM writes WHERE thread_id = %(thread_id)s AND thread_ts = %(thread_ts)s\",\n",
" {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": thread_ts,\n",
" },\n",
" )\n",
" # deserialize the checkpoint and metadata\n",
" return CheckpointTuple(\n",
" config=config,\n",
" checkpoint=self.serde.loads(checkpoint),\n",
" metadata=self.serde.loads(metadata),\n",
" parent_config={\n",
" \"configurable\": {\n",
" \"thread_id\": thread_id,\n",
" \"thread_ts\": parent_ts,\n",
" }\n",
" }\n",
" if parent_ts\n",
" else None,\n",
" pending_writes=[\n",
" (task_id, channel, self.serde.loads(value))\n",
" async for task_id, channel, value in cur\n",
" ]\n",
" )\n",
"\n",
" def _search_where(\n",
" self,\n",
@@ -701,10 +769,10 @@
{
"data": {
"text/plain": [
"{'messages': [HumanMessage(content=\"what's the weather in sf\", id='8481d24b-d505-40fc-8c55-347277d50f2a'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_cGApaEshW0YNp5WsirAcFTBv', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-bf85107a-02a4-48d1-83ba-5448498f5320-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_cGApaEshW0YNp5WsirAcFTBv'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='f685da3d-b196-44a1-9fe5-c7ce4d3d7d1d', tool_call_id='call_cGApaEshW0YNp5WsirAcFTBv'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'stop', 'logprobs': None}, id='run-0d681a28-8116-4cb2-a25f-e4f7949481eb-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}"
"{'messages': [HumanMessage(content=\"what's the weather in sf\", id='bc87fac7-1da1-4818-a43b-6ba7c9b9b3e4'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b9de0cab-f310-4f74-897e-97014072c001-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='8d8f9596-a683-4644-a898-1e303b5a01ea', tool_call_id='call_MjkmibJlXeuNchL6B8qpIjOW'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-4b5282a3-e7a6-42ee-ad0f-e6013a745a88-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}"
]
},
"execution_count": 8,
@@ -726,12 +794,12 @@
"data": {
"text/plain": [
"{'v': 1,\n",
" 'ts': '2024-06-27T01:06:18.970784+00:00',\n",
" 'id': '1ef34217-5fac-63f8-8003-9f1ea087dd14',\n",
" 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='8481d24b-d505-40fc-8c55-347277d50f2a'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_cGApaEshW0YNp5WsirAcFTBv', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-bf85107a-02a4-48d1-83ba-5448498f5320-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_cGApaEshW0YNp5WsirAcFTBv'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='f685da3d-b196-44a1-9fe5-c7ce4d3d7d1d', tool_call_id='call_cGApaEshW0YNp5WsirAcFTBv'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'stop', 'logprobs': None}, id='run-0d681a28-8116-4cb2-a25f-e4f7949481eb-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})],\n",
" 'ts': '2024-07-12T15:21:51.891852+00:00',\n",
" 'id': '1ef40627-6fb2-6962-8003-b74d816658c5',\n",
" 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='bc87fac7-1da1-4818-a43b-6ba7c9b9b3e4'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b9de0cab-f310-4f74-897e-97014072c001-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_MjkmibJlXeuNchL6B8qpIjOW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='8d8f9596-a683-4644-a898-1e303b5a01ea', tool_call_id='call_MjkmibJlXeuNchL6B8qpIjOW'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-4b5282a3-e7a6-42ee-ad0f-e6013a745a88-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})],\n",
" 'agent': 'agent'},\n",
" 'channel_versions': {'__start__': 2,\n",
" 'messages': 5,\n",
@@ -790,7 +858,7 @@
{
"data": {
"text/plain": [
"CheckpointTuple(config={'configurable': {'thread_id': '2', 'thread_ts': '2024-06-27T01:06:20.104263+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:20.104263+00:00', 'id': '1ef34217-6a7b-687e-8003-ab84b18fa9dc', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='3ef79bb9-4249-419d-b142-8d18de63c2eb'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_zqfYbNrD30Qa9YHFSI7nJVJN', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-5dd2d9d1-e291-43e4-86cd-96a50a546f48-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_zqfYbNrD30Qa9YHFSI7nJVJN'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='f3eadc9a-9340-41d3-a0fb-521a78e6c438', tool_call_id='call_zqfYbNrD30Qa9YHFSI7nJVJN'), AIMessage(content='The weather in San Francisco is sunny! 🌞', response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 84, 'total_tokens': 95}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'stop', 'logprobs': None}, id='run-f897e17e-8f0b-4140-be91-35f351609d82-0', usage_metadata={'input_tokens': 84, 'output_tokens': 11, 'total_tokens': 95})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in San Francisco is sunny! 🌞', response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 84, 'total_tokens': 95}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'stop', 'logprobs': None}, id='run-f897e17e-8f0b-4140-be91-35f351609d82-0', usage_metadata={'input_tokens': 84, 'output_tokens': 11, 'total_tokens': 95})]}}}, parent_config={'configurable': {'thread_id': '2', 'thread_ts': '1ef34217-659f-62ec-8002-7f9b6b4c2a2f'}})"
"CheckpointTuple(config={'configurable': {'thread_id': '2', 'thread_ts': '1ef40627-7d58-6422-8003-de6e83a8c293'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:53.322868+00:00', 'id': '1ef40627-7d58-6422-8003-de6e83a8c293', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='8d0209ed-a8c2-42ae-8e77-cc71a9cca29d'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_BO4zHHp0JkEWtrtaEqFHkDjK', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-1f76b350-6a33-4de7-9276-59725b1ac101-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_BO4zHHp0JkEWtrtaEqFHkDjK', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='c1bb1a24-62a8-4b43-b90e-b00899c112a8', tool_call_id='call_BO4zHHp0JkEWtrtaEqFHkDjK'), AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-7576d437-4938-48b9-b2cf-e4809d92742d-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_dd932ca5d1', 'finish_reason': 'stop', 'logprobs': None}, id='run-7576d437-4938-48b9-b2cf-e4809d92742d-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}}, parent_config={'configurable': {'thread_id': '2', 'thread_ts': '1ef40627-775a-6746-8002-a3967bf0eae6'}}, pending_writes=[])"
]
},
"execution_count": 11,
@@ -879,7 +947,7 @@
{
"data": {
"text/plain": [
"CheckpointTuple(config={'configurable': {'thread_id': '3', 'thread_ts': '2024-06-27T01:06:21.430879+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:21.430879+00:00', 'id': '1ef34217-7722-6438-8003-0a35c0f65f25', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='cb5a1b8c-f329-4bee-857a-5d2c3d020147'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_gPUiz98ETlDNXttk98w6iThc', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2d35ac1c-0a99-4538-a8fc-4c182d9df69a-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_gPUiz98ETlDNXttk98w6iThc'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='f5f80d01-92e8-4961-b2b0-3b4d190c4d26', tool_call_id='call_gPUiz98ETlDNXttk98w6iThc'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None}, id='run-aea6306e-f6cc-4f87-aaf4-b0dcfbdf9684-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None}, id='run-aea6306e-f6cc-4f87-aaf4-b0dcfbdf9684-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '3', 'thread_ts': '1ef34217-7099-68be-8002-1a2b75cf9196'}})"
"CheckpointTuple(config={'configurable': {'thread_id': '3', 'thread_ts': '1ef40627-8b0e-6b02-8003-68a7a04ea6a5'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.760751+00:00', 'id': '1ef40627-8b0e-6b02-8003-68a7a04ea6a5', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='108ac72d-f658-4ae0-af57-af481adc8aa5'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_XY5TyZEwF5nbdNTWjjiqGtdS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-454e2142-6f18-4676-ac4b-91f89ea7a6d4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_XY5TyZEwF5nbdNTWjjiqGtdS', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='2d86514f-b8f0-439b-ab94-68c731309c63', tool_call_id='call_XY5TyZEwF5nbdNTWjjiqGtdS'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-90ae3622-b480-4964-b689-9c1a572112f1-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-90ae3622-b480-4964-b689-9c1a572112f1-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '3', 'thread_ts': '1ef40627-860c-63d4-8002-49a92ae87052'}}, pending_writes=[])"
]
},
"execution_count": 15,
@@ -927,11 +995,11 @@
{
"data": {
"text/plain": [
"[CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.556635+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:22.556635+00:00', 'id': '1ef34217-81de-6c5a-8003-04d6ed02b672', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='9df9dda1-5e80-4938-bf24-9207738d9f5f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TJWuPJovXruJVafVcI540OfS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-5ba0db1c-45e7-4389-975f-489135ff50b4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TJWuPJovXruJVafVcI540OfS'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='04dd3f8a-c063-4885-bfcd-901aaeb63797', tool_call_id='call_TJWuPJovXruJVafVcI540OfS'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None}, id='run-de373cba-dc06-4603-bed1-ba4534a2a45a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None}, id='run-de373cba-dc06-4603-bed1-ba4534a2a45a-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.556635+00:00'}}),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.010058+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:22.010058+00:00', 'id': '1ef34217-7ca8-64ca-8002-c710e3b07a9c', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='9df9dda1-5e80-4938-bf24-9207738d9f5f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TJWuPJovXruJVafVcI540OfS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-5ba0db1c-45e7-4389-975f-489135ff50b4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TJWuPJovXruJVafVcI540OfS'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='04dd3f8a-c063-4885-bfcd-901aaeb63797', tool_call_id='call_TJWuPJovXruJVafVcI540OfS')], 'tools': 'tools'}, 'channel_versions': {'__start__': 2, 'messages': 4, 'start:agent': 3, 'agent': 4, 'branch:agent:should_continue:tools': 4, 'tools': 4}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 2, 'writes': {'tools': {'messages': [ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='04dd3f8a-c063-4885-bfcd-901aaeb63797', tool_call_id='call_TJWuPJovXruJVafVcI540OfS')]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.010058+00:00'}}),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.004456+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:22.004456+00:00', 'id': '1ef34217-7c9a-6a64-8001-65cb5b71ff36', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='9df9dda1-5e80-4938-bf24-9207738d9f5f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TJWuPJovXruJVafVcI540OfS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-5ba0db1c-45e7-4389-975f-489135ff50b4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TJWuPJovXruJVafVcI540OfS'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})], 'agent': 'agent', 'branch:agent:should_continue:tools': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 3, 'start:agent': 3, 'agent': 3, 'branch:agent:should_continue:tools': 3}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TJWuPJovXruJVafVcI540OfS', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4008e3b719', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-5ba0db1c-45e7-4389-975f-489135ff50b4-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_TJWuPJovXruJVafVcI540OfS'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:22.004456+00:00'}}),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:21.494039+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:21.494039+00:00', 'id': '1ef34217-77bc-67e0-8000-0dcc15e3e392', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='9df9dda1-5e80-4938-bf24-9207738d9f5f')], 'start:agent': '__start__'}, 'channel_versions': {'__start__': 2, 'messages': 2, 'start:agent': 2}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 0, 'writes': None}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:21.494039+00:00'}}),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '2024-06-27T01:06:21.491815+00:00'}}, checkpoint={'v': 1, 'ts': '2024-06-27T01:06:21.491815+00:00', 'id': '1ef34217-77b7-6114-bfff-cceb4fa6453c', 'channel_values': {'messages': [], '__start__': {'messages': [['human', \"what's the weather in nyc\"]]}}, 'channel_versions': {'__start__': 1}, 'versions_seen': {}, 'pending_sends': []}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [['human', \"what's the weather in nyc\"]]}}, parent_config=None)]"
"[CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-96b9-6682-8003-134aebfec1e9'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.984109+00:00', 'id': '1ef40627-96b9-6682-8003-134aebfec1e9', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU'), AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-924e10c9-7005-4cbf-a92e-3ce63b54092f-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})], 'agent': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 5, 'start:agent': 3, 'agent': 5, 'branch:agent:should_continue:tools': 4, 'tools': 5}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 3, 'tools': 4}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='The weather in NYC might be cloudy.', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 88, 'total_tokens': 97}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'stop', 'logprobs': None}, id='run-924e10c9-7005-4cbf-a92e-3ce63b54092f-0', usage_metadata={'input_tokens': 88, 'output_tokens': 9, 'total_tokens': 97})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-96b9-6682-8003-134aebfec1e9'}}, pending_writes=None),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-91a0-6100-8002-e404dda477d4'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.449447+00:00', 'id': '1ef40627-91a0-6100-8002-e404dda477d4', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73}), ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU')], 'tools': 'tools'}, 'channel_versions': {'__start__': 2, 'messages': 4, 'start:agent': 3, 'agent': 4, 'branch:agent:should_continue:tools': 4, 'tools': 4}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {'branch:agent:should_continue:tools': 3}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 2, 'writes': {'tools': {'messages': [ToolMessage(content='It might be cloudy in nyc', name='get_weather', id='50e612d7-c770-44dd-b128-4bfdbd7d5b7d', tool_call_id='call_pS4ybOXkIDOmS93jZ8wOYGfU')]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-91a0-6100-8002-e404dda477d4'}}, pending_writes=None),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-9194-66de-8001-86c8d77c2d7c'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:55.444687+00:00', 'id': '1ef40627-9194-66de-8001-86c8d77c2d7c', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})], 'agent': 'agent', 'branch:agent:should_continue:tools': 'agent'}, 'channel_versions': {'__start__': 2, 'messages': 3, 'start:agent': 3, 'agent': 3, 'branch:agent:should_continue:tools': 3}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {'start:agent': 2}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'function': {'arguments': '{\"city\":\"nyc\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 58, 'total_tokens': 73}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d33f7b429e', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-07c7ee03-64f7-462a-9249-615572156216-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'nyc'}, 'id': 'call_pS4ybOXkIDOmS93jZ8wOYGfU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 58, 'output_tokens': 15, 'total_tokens': 73})]}}}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-9194-66de-8001-86c8d77c2d7c'}}, pending_writes=None),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b8a-6b1c-8000-55b423aa733b'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.811566+00:00', 'id': '1ef40627-8b8a-6b1c-8000-55b423aa733b', 'channel_values': {'messages': [HumanMessage(content=\"what's the weather in nyc\", id='1c1e48ba-fa25-4190-a847-459828f44579')], 'start:agent': '__start__'}, 'channel_versions': {'__start__': 2, 'messages': 2, 'start:agent': 2}, 'versions_seen': {'__start__': {'__start__': 1}, 'agent': {}, 'tools': {}}, 'pending_sends': []}, metadata={'source': 'loop', 'step': 0, 'writes': None}, parent_config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b8a-6b1c-8000-55b423aa733b'}}, pending_writes=None),\n",
" CheckpointTuple(config={'configurable': {'thread_id': '4', 'thread_ts': '1ef40627-8b88-62b8-bfff-9922bbf9342b'}}, checkpoint={'v': 1, 'ts': '2024-07-12T15:21:54.810527+00:00', 'id': '1ef40627-8b88-62b8-bfff-9922bbf9342b', 'channel_values': {'messages': [], '__start__': {'messages': [['human', \"what's the weather in nyc\"]]}}, 'channel_versions': {'__start__': 1}, 'versions_seen': {}, 'pending_sends': []}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [['human', \"what's the weather in nyc\"]]}}, parent_config=None, pending_writes=None)]"
]
},
"execution_count": 17,
+1 -1
View File
@@ -252,7 +252,7 @@
"id": "5324ea49-5745-47b5-a0a5-bf58c8babe46",
"metadata": {},
"outputs": [],
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
},
{
"cell_type": "markdown",
@@ -42,7 +42,7 @@
"metadata": {},
"outputs": [
{
"name": "stdout",
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
@@ -80,7 +80,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 3,
"id": "d59234f9-173e-469d-a725-c13e0979663e",
"metadata": {},
"outputs": [],
@@ -181,19 +181,16 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 4,
"id": "b90941d8-afe4-42ec-9262-9c3b87c3b1ec",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from langchain_core.runnables import RunnableLambda\n",
"from langchain_core.callbacks import adispatch_custom_event\n",
"\n",
"async def get_items(place: str) -> str:\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
" # NOTE: we need to define a special langchain runnable that we'll be using for logging the streaming outputs from within a tool\n",
" tool_logger = RunnableLambda(lambda inputs: inputs).with_config({\"tags\": [\"tool_call\"]})\n",
"\n",
" # this can be replaced with any actual streaming logic that you might have\n",
" def stream(place: str):\n",
" if \"bed\" in place: # For under the bed\n",
@@ -205,7 +202,13 @@
"\n",
" tokens = []\n",
" for token in stream(place):\n",
" tool_logger.invoke(token)\n",
" await adispatch_custom_event(\n",
" # this will allow you to filter events by name\n",
" \"tool_call_token_stream\",\n",
" {\"function_name\": \"get_items\", \"arguments\": {\"place\": place}, \"tool_output_token\": token},\n",
" # this will allow you to filter events by tags\n",
" config={\"tags\": [\"tool_call\"]}\n",
" )\n",
" tokens.append(token)\n",
"\n",
" return \", \".join(tokens)\n",
@@ -245,7 +248,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 5,
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
"metadata": {},
"outputs": [],
@@ -284,10 +287,18 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 6,
"id": "45c96a79-4147-42e3-89fd-d942b2b49f6c",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
" warn_beta(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
@@ -301,8 +312,8 @@
"source": [
"async for event in graph.astream_events({\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_chain_end\" and \"tool_call\" in tags:\n",
" print(\"Tool token\", event[\"data\"][\"output\"])"
" if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n",
" print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"
]
}
],
+21 -15
View File
File diff suppressed because one or more lines are too long
+9 -6
View File
@@ -555,6 +555,8 @@ class Pregel(
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# update channels
with ChannelsManager(self.channels, checkpoint, config) as channels:
# create task to run all writers of the chosen node
@@ -644,6 +646,8 @@ class Pregel(
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# update channels, acting as the chosen node
async with AsyncChannelsManager(self.channels, checkpoint, config) as channels:
# create task to run all writers of the chosen node
@@ -987,7 +991,7 @@ class Pregel(
else:
# no input is taken as signal to proceed past previous interrupt
checkpoint = copy_checkpoint(checkpoint)
for k in self.stream_channels_list:
for k in channels:
if k in checkpoint["channel_versions"]:
version = checkpoint["channel_versions"][k]
checkpoint["versions_seen"][INTERRUPT][k] = version
@@ -1444,7 +1448,7 @@ class Pregel(
else:
# no input is taken as signal to proceed past previous interrupt
checkpoint = copy_checkpoint(checkpoint)
for k in self.stream_channels_list:
for k in channels:
if k in checkpoint["channel_versions"]:
version = checkpoint["channel_versions"][k]
checkpoint["versions_seen"][INTERRUPT][k] = version
@@ -1799,11 +1803,10 @@ def _should_interrupt(
# defaultdicts are mutated on access :( so we need to copy
seen = checkpoint["versions_seen"].copy()[INTERRUPT]
return (
# interrupt if any of snapshopt_channels has been updated since last interrupt
# interrupt if any channel has been updated since last interrupt
any(
checkpoint["channel_versions"].get(chan, null_version)
> seen.get(chan, null_version)
for chan in snapshot_channels
version > seen.get(chan, null_version)
for chan, version in checkpoint["channel_versions"].items()
)
# and any triggered node is in interrupt_nodes list
and any(
+2 -2
View File
@@ -134,7 +134,7 @@ def print_step_tasks(step: int, next_tasks: list[PregelExecutableTask]) -> None:
print(
f"{get_colored_text(f'[{step}:tasks]', color='blue')} "
+ get_bolded_text(
f"Starting step {step} with {n_tasks} task{'s' if n_tasks > 1 else ''}:\n"
f"Starting step {step} with {n_tasks} task{'s' if n_tasks != 1 else ''}:\n"
)
+ "\n".join(
f"- {get_colored_text(name, 'green')} -> {pformat(val)}"
@@ -153,7 +153,7 @@ def print_step_writes(
print(
f"{get_colored_text(f'[{step}:writes]', color='blue')} "
+ get_bolded_text(
f"Finished step {step} with writes to {len(by_channel)} channel{'s' if len(by_channel) > 1 else ''}:\n"
f"Finished step {step} with writes to {len(by_channel)} channel{'s' if len(by_channel) != 1 else ''}:\n"
)
+ "\n".join(
f"- {get_colored_text(name, 'yellow')} -> {', '.join(pformat(v) for v in vals)}"
+3 -3
View File
@@ -1746,13 +1746,13 @@ langchain-core = ">=0.2.2rc1,<0.3"
[[package]]
name = "langchain-core"
version = "0.2.15"
version = "0.2.16"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langchain_core-0.2.15-py3-none-any.whl", hash = "sha256:3bf7afaef96d7c1af0d9d223833bdee5fafc46755dc10f9c7576a85d4f6c5240"},
{file = "langchain_core-0.2.15.tar.gz", hash = "sha256:ce03ab0a5c45b4ebfe5475eb07bf081cd21218421ff4cf26b8d2e5573ae2bd42"},
{file = "langchain_core-0.2.16-py3-none-any.whl", hash = "sha256:68dd6fbdd8d704e124ee14c39d0b0dcf38a47bb32234328b63ad2e98385b1c80"},
{file = "langchain_core-0.2.16.tar.gz", hash = "sha256:cc03083e059bff86ca3e6ba60097f91d7765a929edd04eb226bb20c208447caf"},
]
[package.dependencies]
File diff suppressed because one or more lines are too long
+76
View File
@@ -0,0 +1,76 @@
from typing import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
def test_interruption_without_state_updates():
"""Test interruption without state updates. This test confirms that
interrupting doesn't require a state key having been updated in the prev step"""
class State(TypedDict):
input: str
def noop(_state):
pass
builder = StateGraph(State)
builder.add_node("step_1", noop)
builder.add_node("step_2", noop)
builder.add_node("step_3", noop)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
memory = MemorySaver()
graph = builder.compile(checkpointer=memory, interrupt_after="*")
initial_input = {"input": "hello world"}
thread = {"configurable": {"thread_id": "1"}}
graph.invoke(initial_input, thread, debug=True)
assert graph.get_state(thread).next == ("step_2",)
graph.invoke(None, thread, debug=True)
assert graph.get_state(thread).next == ("step_3",)
graph.invoke(None, thread, debug=True)
assert graph.get_state(thread).next == ()
async def test_interruption_without_state_updates_async():
"""Test interruption without state updates. This test confirms that
interrupting doesn't require a state key having been updated in the prev step"""
class State(TypedDict):
input: str
async def noop(_state):
pass
builder = StateGraph(State)
builder.add_node("step_1", noop)
builder.add_node("step_2", noop)
builder.add_node("step_3", noop)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
memory = MemorySaver()
graph = builder.compile(checkpointer=memory, interrupt_after="*")
initial_input = {"input": "hello world"}
thread = {"configurable": {"thread_id": "1"}}
await graph.ainvoke(initial_input, thread, debug=True)
assert (await graph.aget_state(thread)).next == ("step_2",)
await graph.ainvoke(None, thread, debug=True)
assert (await graph.aget_state(thread)).next == ("step_3",)
await graph.ainvoke(None, thread, debug=True)
assert (await graph.aget_state(thread)).next == ()
+4
View File
@@ -29,6 +29,7 @@ from langgraph_sdk.schema import (
GraphSchema,
Metadata,
MultitaskStrategy,
OnConflictBehavior,
Run,
StreamMode,
Thread,
@@ -344,6 +345,7 @@ class ThreadsClient:
*,
metadata: Metadata = None,
thread_id: Optional[str] = None,
if_exists: Optional[OnConflictBehavior] = None,
) -> Thread:
"""Create a new thread."""
payload: Dict[str, Any] = {}
@@ -351,6 +353,8 @@ class ThreadsClient:
payload["thread_id"] = thread_id
if metadata:
payload["metadata"] = metadata
if if_exists:
payload["if_exists"] = if_exists
return await self.http.post("/threads", json=payload)
async def update(self, thread_id: str, *, metadata: dict[str, Any]) -> Thread:
+2
View File
@@ -11,6 +11,8 @@ StreamMode = Literal["values", "messages", "updates", "events", "debug"]
MultitaskStrategy = Literal["reject", "interrupt", "rollback", "enqueue"]
OnConflictBehavior = Literal["raise", "do_nothing"]
All = Literal["*"]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.25"
version = "0.1.26"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"