docs: functional api (#3125)

* Concepts page for the functional API
* How-to guides that show functional API implementations
* API reference for entrypoint, task, entrypoint.final
* Add functional API version to the workflows

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
Co-authored-by: ccurme <chester.curme@gmail.com>
This commit is contained in:
Eugene Yurtsev
2025-01-27 16:31:43 -05:00
committed by GitHub
co-authored by Vadym Barda ccurme
parent a910a1a341
commit 90f1e66748
45 changed files with 5565 additions and 1264 deletions
+6 -11
View File
@@ -1,17 +1,11 @@
import importlib
import inspect
import logging
import os
import re
from typing import List, Literal, Optional
from typing_extensions import TypedDict
from functools import lru_cache
from typing import List, Literal, Optional
import nbformat
from nbconvert.preprocessors import Preprocessor
from typing_extensions import TypedDict
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -52,6 +46,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "Command", "types"),
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
(["langgraph.func"], "langgraph.func", "task", "func"),
([], "langgraph.types", "RetryPolicy", "types"),
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
@@ -88,8 +84,6 @@ _IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
@@ -109,6 +103,7 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
return None
def _get_doc_title(data: str, file_name: str) -> str:
try:
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
@@ -287,4 +282,4 @@ def update_markdown_with_imports(markdown: str) -> str:
# Apply the replace_code_block function to all matches in the markdown
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
return updated_markdown
return updated_markdown
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
eNqNVVtMHFUYXnoleKnGSL01nG4qJHVnd2Z2C+z6oLBQ2yIXYZtKUcnZmcPOwMyc6cxZ2uVmhPqk0M5TE/WpLLtmxQKxalP1SUiqVmyaNkqjyIsxhgRjfCs2eGbZ5VJu3aeZ8//f/33//51/tjfZjgxTxlrOsKwRZECB0BfT6k0a6FQUmeRsQkVEwmK8rrYhNBg15KnnJUJ0M+DxQF12Q41IBtZlwS1g1dPOeVRkmjCCzHgYi7E7ObmdThWeaSa4DWmmMwA4lve5gDObRU+aOp0GVhB9ckZNZDhpVMBUikbsI0neD44WqSCMw85uF1jOhaYpm4TS3wc4IoNyHF4EBRUYFZEb1MgCAgQDFSECYji6HxzBp4EANXAUSEjR7TMaF2HspdUk6wg6LUFSZAI1BjSoIpr/pt0OFpFiR4U0I+NlDjEm1jREGAUSOkVnd1JCUKSjPheXsEmssTXDG4GCgHTCIE3AoqxFrE8jHbLuAiJqsWukBLte2h0r1YaQzkBFbkeJRZQ1CnVdkQVoxz2tlHs4o5khMR2tDafs1hhqgUasL8uyOjx1Meq1Bli31+fmR88wdMKyplCzaB9UUkJPx79aGdCh0EbrMJl7ZCUWwZdW5mDTGqqGQm3DqpLQECRrCBpqse+zledGVCOyiqxksG4tXSa4TOd1c5zbP7aqsBnTBGuoBSomGlsa8hIkxbO8l2GLGZa7lJ2SgrQIkaxBvtj3sYFMne4A6kvQkiRq9sapI+j6tWTm1l6srcq6Oe3YG6+g7ljfnECiC/A8OEZvFa1/CPBcgPUHOA68Uh0aDmZoQuuaMRYyoGa2UEMqs+YnBSmqtSExFVzX9innclsG5VdkVSZMZmWpWfarFfexLDtVuGmmgVQ6NZsx7vX7/VvUpZNBxLps90eHx/B8KNMle3J9HlnTo/QKprc/oyphq6K6Dm6Zv6wtiyl8AMwGCrmTU0XroXGUrJE4VJpme2Hr/GWJGUzRg2A2lgjWg983vkWiA5tkrhzcYjbYNHtDPamM84wsWl/T52aWa2wMHe84Vl7devzVGkmr9tbprWYJGmyXoZXi3ByIYBxR0EjwMBOEgoSYhvQKWcmKxpqy6qPB4deZehzG9C6FIL1zGtZQogEZdDWtlKDgqEg/dgZKUHh9WaN12c8W86XFLB+GXlQi8j6m8kT9aHaZlpYlbn8p038479CVNejRRE5OwXu5jvRvu3iurGr85YffXfj+/L+hOyc95huP7ZmdftLiJ8d/u6TWFz09ytz7++bEzomGmfnpzvM7r+Y+usv/Bfr87dLrt7u6uj6Y++mvy1Xeu9/27Hgo0H9lqG684HHXgbmSmcrAa7V5+bsGfunruzH/3NgVZi4POm9ddM8mDs4+49pRWFmve36Ihv5p+6R5oC8y+d35ghsk3P5fS/9e76niP12tgZvj3O73n63qvTf/yPBIfkVMeaIDbtt9ZOz2R7k9Z2cih/NyB4zfm65d8Py6bV9/Y9Mfew7k/Hwrv3dPkRIsH9nXMF9S91Z3ecHCi5N3Pwz9eLVnu8OxsLDdIZY/lWrOcTj+B1mCCwM=
@@ -0,0 +1 @@
eNqNVGtMHFUUXoQ0FavRGAt/bCcThCrMMjMsWxarlAIlpVIorKW8bG5nLuzAvJjH8rKmYKkxlbRDUxoTCElZdulKYRGMTaiExpKYtFFrDbAxaeIfpRok0USNreDdZReovDrJJDP3nO+c73znnNvqcUJF5SQxYoATNagARkM/qtHqUWCdDlXtjFuAmkNiXYUFxfZeXeH8rzg0TVbTk5OBzJmBqDkUSeYYMyMJyU4qWYCqCqqh6jopsY3++mZcAA0nNKkWiiqejlEkbUnC8LATOilvxhWJh+gL11Wo4MjKSIiJqAWO6h1AS1AxoRETgQAz8FOVAbTEQj5gZXigs5BIIVIJVRJFqBE80BBn/JTHAQGLCjvvckiqZgyvoToEGAbKGgFFRmI5sdq4Vt3EyUkYC6sCMbxMIF5QC8NbC6FMAJ5zQvcSyvABWeY5BgTsyTUo90CIM6E1ynCt2RsojUAVi5rxeWaYR3JhI1JWxEhzisVM+xoIVQOcyCNtUB2IklsO2sdWG2TA1KI4RKhrhnsJPLjaR1KNvnzAFBQ/FhIojMPoA4pgtYysPld0UeMEaHiyCtemCxlX0qWYKcpsG34ssNooMkZfFeBVOLws8jLES5N0CkFaCZIaDKvEQ7Facxi9FGXrV6Aqo4mD77tRSE1XW12oI/DOV57QkFwpOBzu5n1TjCsbdcf4ogSySRhNY3lAxFD8VIym0klbOkVjufn2gaxQGvu6zRi2K0BUq1BDcsLN9zAOXayFrDdr3bb78ZWyFJSf5wROI0ILgpoV+DVcFpIk/fGbeipQQKoFMrpSbDbbFnGRMlAzRgP1IfEImraHqqTK1s/DibKORjC4bCFW7gArxOu1Lf1XuIUx8U+A2YAhXeZPWA8t6doain1pwWyJW/uvUAxhEp4EszFFbD34/+RbShS3iedq4Za8sU29N+TjDXWe4FjjBvo+QVJHGw5AzuI8mi2WHM+rLSnJrSm0C1KvkwOGlzJTWLUkVfNwKOsgkQUYBySKgytkeLJLj2TmH8oaOE4USSclNEt2gGZOlEToLoYKWk3Dy/CSzqLLToFuBC/KLDVGbaSVTrOSaawtjdzL0hYip6TIF16m5WVxBW7K4PXeglZWQUeTEW/vPrfdFHwi0bu4yF7IPxyZuePsYt6dyduDo+VvXI95tb/w23Otu7I766z6B1/H6x1XO79bNI+V9TdE75vPPeCMLRsYnv6riu7O2Nc01nP3J+fDR1pGzPi/12+Mf/mUMOa+meP9rds3EVdnOpLab7Mm+S534lOzvbeJSyO/X7GD+MRPDulvdttf2jnb1j7je8//cl77+Nm/02oqTv/RWflRuaOt4NljST2+S12nKwsu2Dp+MU3sOTj7IErsmp94RtpzdQTb0XMzffbp7yvoNojPRRe2TEq1P1ujWi53/NNzpvkiz8zd3TZXgW93bvumg3n909hjUfHPz3SM+u81dBVVvPPjwmf7U0v3ftgVzbTfEhgbtRCdt/vjmRenupgk8vziQH1sQ/c1qvSFxKl73v3z7+a2F1h2zTxKfeu5Jnx6YTruB8o29OdDKahdpOnXnbfuP4gwmf4DBVoOEg==
@@ -0,0 +1 @@
eNqNVF1sFFUUbmlTEYPBYCTGGIahpQY6uzOz28q2prpua2Vr6d+G/gji7czt7tCZucPMncqKgJZiNI3iGAzExBe7P82mlFZRVGqEGgTUpn0huCiYJhp5MvEBmz7VO9vdttilZZ7u3HO+c77znXNuT7wb6oaE1NxBScVQBwImP4bVE9fhfhMauDemQBxCYqShvjnQb+pSsiiEsWaUO51AkxxAxSEdaZLgEJDi7OacCjQMEIRGpAOJ4aRxkFbAgb0YdUHVoMspjuXdJRSdcSI3Lx+kdSRDcqJNA+o0sQqIMFGxfRWSNlE7ihWqA3XQh/bYSCRC2bYIMjBFyLiYUsZAqgoxIwNM+NKH4iEIRFLU8UgIGdgaWULzDBAEqGEGqgISJTVonQ6+LmkllAg77RgJwY6X0sFKdEGoMUCWumFsDmUNA02TJQHYduc+knswzZfBYQ0uNSfsshhSrYqtc94MD2dDmKiqUqzD5XbwwwcYAwNJlYkupA5CKaal7OcXGzQgdJE4TLpjVmwOPLTYBxlWtA4I9c13hQS6ELKiQFfK3J8vvtdNFUsKtOK+hqXp0saFdC4Hxzk8I3cFNsKqYEU7gWzAkXmR5yEJnuVdDFvGsNxQRiUZqkEcsvo5rnRAh4ZGpg0ejZGQ2DR6IqQj8Ocr8fSAfFpfm+nmrZwNkSrSHevbFiiWUDxP+YFKkfilFM+Vs55yjqVq6gKDvnSaQNZmjAR0oBqdpCHVmebHhZCpdkEx4cva9iS9UJZO8suSImEmvRykWfavFXGzLJvcsqynDhWimp0x4vJ4PCvEJcpAbJ216yPiMTwfmKuS9bRnzyOpmklGMLVoaVYxmxXhtXVF/wVuGcyW+8BkZ8ix7cnibGhk4iUUo9tT2bat7L9AMY0pvh/MvSlS2eD/k28uUeEynouFm/OmlvW+J59EuvOMJFqj5LyX5ZqQx2sohrC/1CeJ1Ri5db/ZsLO/WwJWgnNwVBChoAzP+F5gfEAIQaY5tUJWvKptp7duh2+wlWlCHYjMUgCQmVORCmPNUCeraSUEGZkieex0GCPwJm+bddbDlvHbSzuhS+Q6nhZ5N1Pd0jScWab5ZYnYL2XqaX+LrKxOri7lrtvYtzon9eWJx7213z+37tjsj92OV8o+lO98tsHbeKNvYPUaGtx88tfo1Zc+/iT69uSs48HHHq3P+3361tHHNxUMT8bq9uw6F579qqL35PWnJl6rv6nPJHdN5bqvTE6dGDvxW4HXNzGev9vPvbs5dy3zgHyhB+1+uLWk7yeae8QP/EbjRz+8WTBR+OKqf5Vn3ltzuO7OTGtrtTm+8aEvKruujY/iob/7a4oG26sa8/461v6PN1yz7/SXv+D1zWMX17Zob1z/oPf8NzUnT11Y1TRQNJ3/LD1VuTmcX/u15P5jpqji+Yijlhn1+V/d1n+x4rtLN6ZvF/LXjuT9eXX69tiRy22H1wvvX0ak9NnZvJzkE+9Uyrk5Of8BO5PlsA==
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
eNptVQ1QHNUdh8QPaquD40czNtbNxUSL7LJ3u/eJmMLxESAHOQ5CQlTy2H17t9x+sbt3cCCmJbFOKzKuNZnEYC3kuEMkJA6EfFjiRzR1rM3UFtNCpNbWqeOMOrVaMWmm9O1xZ2CSnbm7fe/93v//e///736vJxmFqsbLUvYoL+lQBYyOBprRk1RhWwRq+u6ECPWQzMY31wbqD0ZUfiYvpOuK5ikoAApPyAqUAE8wslgQtRYwIaAXoHdFgKkw8RaZjc1mq10WEWoaCELN4sG2d1kYGeWSdDSwNKIt92iYHoJYOwToR8V4CdOAhHEqkBheY+QNlnzMosoCNPERDaqW7ofQjCizUDCngoqO0zIu8hJvIjVdhUBECxwQNIgmdFkW0on1mJKKwkWk1EFN/LfvHqzLIgExBQhCvTnNx8SwUGNUXknDLF4gCJguYwi1jDmnyiIGME2BDM/xDCbIDDD3EGYMBagoOKqxlsqkqKh2qs7DxWEGmhpkaKKj8FLQ0t1tFgD1g1chax7kMtosRAYtt7RCRkfo7oe6kyEIWJTqr1m58ZCs6cbY8m4dBgwDUd2gxMgsymEcCnbySj7GQk4AOhxBHZJgqizGSBhCBQcCH4WJxV3GEaAoAr9IoaBVk6XRdEdxk8uVyyNm13DUf0k3JmoRieLKgs0xJCsJsxJ2N+E80oFrOuAlAckEFwDik1BS6y8vXVAAE0ZB8LRkjcTi5rGlGFkzhnyAqQ0sCwlUJmQMAVV00ONL59WIpPMiNJLezVemSy9eTkcRVivhfGlZYC0mMcZQSmrHlm2GuhrDGRnFMAbIBCPLYR4aM/9ubma45haxyOrb4mq0kwE/wdl8sSAX7uBCxU4HaKhiq4Ua6Ke4YG2grUwL1vhxq5Ny2l1ON23FrQRJWAkrzgQlELM2isQWPbItCmNt9nB9O+Boe32wzAZqiTrGW+sqIWLeCneVvYIqqdaatkJhS1ODg3WUN1s7iJKW8jI/31q5qc0WYZwybPMTbHEhhthFojxbRNh10VGxkffGGkuLRZc/VN4uOVRSFt01TdFYW321r62T3LipuZldSs9B0ziZZuggaRdpPmMZbQhQCuoh4yDlJIdVqCnIIOCuBCqZHtF64kiH8J23kmmjGKytvizh2+OlSJPGVCNk8zGbDatC7mAjbXbMRnmsbg9JYxW++lFvOk39VSX4Uj2yE41DMizLSD7JhCJSGLIj3quKfcoUO+qkSR+5Dw47FFmDeJqVMboVr1u0SLyydHzxn4XLahBIfGcqrTGVUn17Z0c7y0RYNhRtF0l3J03xLTDCcBPpLcgHzDSIEC5qxkG7gxpLr2R0N4LOSuJWEietJztwFZVC4EUe1TP1nfZpzYjbUbGPXwnQ5TBEjp6kU90gTy1FqFBEgjVzXw5Du93u31wdlAlFIYjbSZ1cjtLgUjZWm6gdvxKQDjFIaqMdGTTOs8bM3WjQbIc0BZyQBTRiC5CmGIeVa6E5Djg5CjqoE6YjMiiK2UxFVnVcgwy6lPSYMZMvgg7TY4ooq51yoJMWopuEESIsDERaSmXzDFohpqhQkAF72FuOewETgnggpT8jWbqtpthX6Z3cii8VEl6rLF6ISUnWJJ7jEgGoosYYI4wgR1hklipMoFh1xduMCTfpoGiaBi2sy+biODdegmwoE+1b2cVNp00CAXGPMsZ4iCqyeGiashRiIihyOdDBU9fmTxOL7v/miuvueiInK/WsRJ+Fhd7A+b4DZG73x/eNxm5qII4IsRfzhhomv9/wwvT2vI/2ZffbJ8MH5j4vvzD/9lNZlYEX7sfCZ/spKvhp949z/rW6bsXoYN97TbsP55/qvvTNZ6fzHt355SMfV732zMVL5UT0d13/vWP9rwYmC9nP3+h8rf7QtZ4x9npq1/RA43Prnrj7lO/hcv93p/MqPuSHoiD++rPVPzyb/PLe/vUffvr1W2O3BkNr1m1YUZIzc2znB8l3vnDGWY+7rHVi794c/vnSnFXrHrlm+Oj7nrw7N1331I8+2XzxHFz7Ro5S9fNe/5qfvT537K7nrh8fzp2hmwp2Fv2l/YM3W6cungz9/aM/zUknvrGN/LlywyeTA4PGqJPfPf/FV4Or1mD8nFqtz6ytaxw48etbVkw/43vyjxfOgVeyB1b9p+cHG51HEk/n37ind3vCofyE+nrN8ej/3LYzO97dmtfXd+c/Hn/02pI9s8OHppRbbyBOHLLtmp785ZM33lTz3qznD/2f5U74j0b3B+bvnRJWtw+SD45n98+tLk6uvs17/37f2DB0e94+f9v8sV/4t4AHyMfPlx3of/Hm3N71Cy/vD8/OffXg3oW1G7J6B6L7grM7zs5ceNX7bPFCzsKec13vO753++9XBve5+87Ia3/b4xjsumPH86OnPfcdz383dPr8fPZjZ592//M7c2du3vW33EvZZrNXZrkeKD+KX5OV9X/jgdW5
@@ -0,0 +1 @@
eNqNVgtQVNcZJpq0aFtD05R2SGrubB52gLvcu+8LYoHlISDPXZ5itod7z929cF/cc3dhcbCJIXGaRJLbqRlH01oFIaFIohiNphDTBGIyMYY6aavJmIfGxHaMM9LqdEal5y67EarTyc7s49zz/f///f//nf/sxqEI1JCgyLeNCLIONcDqeIGMjUMa7AhDpPcOSlAPKdxAdZXP3x/WhJPpIV1XUXZWFlAFq6JCGQhWVpGyInQWGwJ6Fv6tijDmZqBV4aKnFu1db5EgQiAIkSWbWLvewio4lqzjhaUBm6xAhB6CRCcE+EsjBJlAQCZ4DcisgFjlF5ZMwqIpIjTxYQQ1S08mMd+JHBbFeRCAkIB0gLfwQ11RxAALRDEeWo+qMRAflmOpmhiBM5+YoADl8BV3RNCaZrHMV9ZoDzvdNeVtxfYGE/aNSTaOLgMp5icI9UCcuIkBWjAsYVZmNMv6FouosMC0acHrFosPp1WcSKvF0mPp6Vn3P7lYqkUIECR4RZOATgBErPQKenRVJrHSpwMdrrLOr4aZ3YIsA98ml5512ERSOCjGMlB10qGQkiALpiukaxBIeIMHIoJx3/+neN+mKhxErCaocZjFi9kRukJg1ILG85oiEYBAKmQFXmCJRPFiKatAw86xRFEskqph6Wm6AOeWCWhskaCJUxHkIC6yWTEsZ0GDZnXW3kCbhUigldY2yOoYjXsyFIKAw6FOJ6UMhBSkG6MLxf4SYFmI6wZlVuFwDGNPsFtQMwkO8iJu0jDupwxjZTGG2yFUSSAKETg4Z2W8DFRVFOYoZLUhRR6J9580udy8PWyKnsTHR9aN/VWYRH5pVnUUn0qZoK1Oxup+uYvEghdkEZ8yUgSYz6Aa239t/oYK2HbshIyfeGNwznh0PkZBxu4KwFb5FrgEGhsydgNNcjnG5j/XwrIuSNAY8lbfHC6+eSOc3UrTVvfeBY5RVGaN3TGpHVxgDHUtSrIK9mHspAZZRWkXoHHyUiDA8oFWKZeuqPc0OClfjZW3VUSDfHsXH8p3u0BdGVcuVsIaOx+s8nUUoWBlDUm77W6nx804aJK2UlbaSpNsUAZRukGy1uvhpgiMdjjb/Z2Adzj9wSIbqLLWst4qT4E16i1hypwl9oJy1NwIxfrmOhfnKg7QXdaC1uKiGqGtdE2HLcy6FdhRY+XycwjMLhwRuFyrU5dcJasFb7ShMF/y1ISKO2WXRikSU9kciXb4yys6uqnVawIBbj49l8NBUnGGLsrhoczXaEIbIpSDesjod3mYFzSIVDxf4WODuGR6GG0cwDqE7x0dis/ZXVXlNyScOlCINWmMN0Auk7DZiDI8hWyUzUnY7Nk0k005iZIK/4g3HsZ/Swnu9eOxhXgsw6KE5IfYUFhuh9yw95ZiHzfFjjtp0sfjioRdqoIgGWdljDSStXM3DFlaODZ3skhFCwJZ6I6FNcZjqu/s7urk2DDHhSKdEsV0O+xCKwyz/P64CZ4DZhhMiJQQLo7TORrfSehuGOdKkTRFUvThLlLDpRAFScD1jH3GrzlkDDhxsV+9GaAr7RBfiEOOWDeoifkIDUpYsGbsG24cDMP86daghCs7hjAu++GFKATns6FtEnr1ZkDcxS4KjXQl0KTAGScfwIsADWg3x/O0A7idre5WjsEi8jgYjqJsDMfZuUPmRGSxF7OZqqLpJIIsvtP1qHEyUwJd5ozJtdNOuwtnmoMvYlYMc9AXbi1UzBxQDqFqUFQA95K3mPQCNgRJX0x/xlBhU2V+Ran3QCM5X0hklTr3f2JIVpAs8PygD2q4McYwKyphDg9LDQ5iX7X5TcZ+hnLZHQ4Hb+c8nIfnGbIAj6GEt29kN2BO2iEgYu4R1hgL2XMt2Q6H3ZJDSCDX48Jtiv3reHRwbvpPLlpy31PJSbHXYvyenX3aV/HMdiplfCZjhNmU/s5v/qyxr9/94qYNA6eWb1vKXM77OO2jV45FTvx8dnzRXYSQvmXxvnPH1ttnpk5/t/eRT5+965dr/9EEua1My8SlLeTy+yY+Pt5Zerljst/y0UrrjtMbNpa++cH2i0ev/vrfT5x/d/W/Cv64jE998e3icOonbz31wMTmVLJa6Nu3c8vUJmNZxs5Cz473+9W0TWNnj55ue+OD/LKJ7/Sm7FNmK3Y8eSZjgMtmitL1vueShR2FyY3k9uTqL3tn/NGt3ydOvNvf1tzwym/vP9JOouLd9y7t/GSDvvy9XbXnDl6Uf/+rw6/dafscPtR/9usr2pXrn30x88LDK8btkrdoT6Pyl+SLZ84X0QV508NfPPfVTGD1tdQLd/wu5Q8HRjL8E8LPLrc/u226WP3B+bR6v7Gi/Mc/6ju/5NDxv6qP2B/3qheuSlzf5K6If/m6nKk7r6UNWDd/eE/nZ+l3Fyw5eIys/Vul28V46tr/mddzoISqb5j68uHZvA+PPtSxYumbdfccadJuH7/s7/v8xOTZJ1ven/ZNf33o9ean95GXHs1uoZvRzpI8Ku3aRXlU6Dzzvbrrz1xNop6Yuv/45iLr3//zlnfb5LkHr2fscR+SfnrvqZ+8QV1ZJI0t67tjD7yy6siF599Oz+pNn07LOFWy6PF3zjCXtsqpP3zs05Srt5n9XpzUw35F2W5PSvovy+lBcQ==
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
+2 -2
View File
@@ -1,5 +1,5 @@
ERROR_FOUND=0
for file in $(find $1 -name "*.ipynb"); do
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
if [ -n "$OUTPUT" ]; then
echo "Errors found in $file"
@@ -10,4 +10,4 @@ done
if [ "$ERROR_FOUND" -ne 0 ]; then
exit 1
fi
fi
+910
View File
@@ -0,0 +1,910 @@
# Functional API
!!! warning "Beta"
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
## Overview
The Functional API is an alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
It allows you to take advantage of LangGraph's key features for [persistence](persistence.md), [human-in-the-loop](human_in_the_loop.md) workflows, and [streaming](streaming.md) without explicitly specifying state, or control flow in terms of nodes and edges.
The **Functional API** and the **[Graph API](./low_level.md)** can be used together in the same application, allowing you to intermix the two paradigms if needed.
## Example
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
```python
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
@task
def write_essay(topic: str) -> str:
"""Write an essay about the given topic."""
time.sleep(1) # A placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
is_approved = interrupt({
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
})
return {
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
}
```
??? example "Detailed Explanation"
This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided.
When the workflow is resumed, it executes from the very start, but because the result of the `write_essay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed.
```python
import time
import uuid
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
from langgraph.checkpoint.memory import MemorySaver
@task
def write_essay(topic: str) -> str:
"""Write an essay about the given topic."""
time.sleep(1) # This is a placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
is_approved = interrupt({
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
})
return {
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
}
thread_id = str(uuid.uuid4())
config = {
"configurable": {
"thread_id": thread_id
}
}
for item in workflow.stream("cat", config):
print(item)
```
```pycon
{'write_essay': 'An essay about topic: cat'}
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
```
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
```python
from langgraph.types import Command
# Get review from a user (e.g., via a UI)
# In this case, we're using a bool, but this can be any json-serializable value.
human_review = True
for item in workflow.stream(Command(resume=human_review), config):
print(item)
```
```pycon
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}
```
The workflow has been completed and the review has been added to the essay.
## Building Blocks
The **Functional API** provides two primitives for building workflows:
- **[Entrypoint](#entrypoint)**: An **entrypoint** is a decorator that designates a function as the starting point of a workflow. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](human_in_the_loop.md).
- **[Task](#task)**: Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously from within an **entrypoint**. Invoking a **task** returns a future-like object, which can be awaited to obtain the result or resolved synchronously.
## Entrypoint
The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./low_level.md#interrupt).
### Definition
An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator.
The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument.
Decorating a function with an `entrypoint` produces a Pregel instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**.
=== "Sync"
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: dict) -> int:
# some logic that may involve long-running tasks like API calls,
# and may be interrupted for human-in-the-loop.
...
return result
```
=== "Async"
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: dict) -> int:
# some logic that may involve long-running tasks like API calls,
# and may be interrupted for human-in-the-loop
...
return result
```
!!! important "Serialization"
The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details.
### Injectable Parameters
When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include:
| Parameter | Description |
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| **previous** | Access the the state associated with the previous `checkpoint` for the given thread. See [state management](#state-management). |
| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](#long-term-memory). |
| **writer** | For streaming custom data, to write custom data to the `custom` stream. Useful for [streaming custom data](#streaming-custom-data). |
| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. |
!!! important
Declare the parameters with the appropriate name and type annotation.
??? example "Requesting Injectable Parameters"
```python
from langchain_core.runnables import RunnableConfig
from langgraph.func import entrypoint
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory
@entrypoint(
checkpointer=checkpointer, # Specify the checkpointer
store=in_memory_store # Specify the store
)
def my_workflow(
some_input: dict, # The input (e.g., passed via `invoke`)
*,
previous: Any = None, # For short-term memory
store: BaseStore, # For long-term memory
writer: StreamWriter, # For streaming custom data
config: RunnableConfig # For accessing the configuration passed to the entrypoint
) -> ...:
```
### Executing
Using the [`@entrypoint`](#entrypoint) yields a Pregel object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
=== "Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(some_input, config) # Wait for the result synchronously
```
=== "Async Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(some_input, config) # Await result asynchronously
```
=== "Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(some_input, config):
print(chunk)
```
=== "Async Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(some_input, config):
print(chunk)
```
### Resuming
Resuming an execution after an [interrupt][langgraph.types.interrupt] can be done by passing a **resume** value to the [Command][langgraph.types.Command] primitive.
=== "Invoke"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(Command(resume=some_resume_value), config)
```
=== "Async Invoke"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(Command(resume=some_resume_value), config)
```
=== "Stream"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
print(chunk)
```
=== "Async Stream"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
print(chunk)
```
**Resuming after an error**
To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config).
=== "Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(None, config)
```
=== "Async Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(None, config)
```
=== "Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(None, config):
print(chunk)
```
=== "Async Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(None, config):
print(chunk)
```
### State Management
When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints).
This allows accessing the state from the previous invocation using the `previous` parameter.
By default, the `previous` parameter is the return value of the previous invocation.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> int:
previous = previous or 0
return number + previous
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(1, config) # 1 (previous was None)
my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation)
```
#### `entrypoint.final`
[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**.
The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
previous = previous or 0
# This will return the previous value to the caller, saving
# 2 * number to the checkpoint, which will be used in the next invocation
# for the `previous` parameter.
return entrypoint.final(value=previous, save=2 * number)
config = {
"configurable": {
"thread_id": "1"
}
}
my_workflow.invoke(3, config) # 0 (previous was None)
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
```
## Task
A **task** represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously. Invoking a **task** returns a future, which can be waited on to obtain the result.
### Definition
Tasks are defined using the `@task` decorator, which wraps a regular Python function.
```python
from langgraph.func import task
@task()
def slow_computation(input_value):
# Simulate a long-running operation
...
return result
```
!!! important "Serialization"
The **outputs** of tasks must be JSON-serializable to support checkpointing.
### Execution
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes). They **cannot** be called directly from the main application code. Calling a **task** produces a future-like object that can be awaited or resolved to obtain the result.
=== "Synchronous Invocation"
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: int) -> int:
future = slow_computation(some_input)
return future.result() # Wait for the result synchronously
```
=== "Asynchronous Invocation"
```python
@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: int) -> int:
return await slow_computation(some_input) # Await result asynchronously
```
## When to use a task
**Tasks** are useful in the following scenarios:
- **Resumable Graph Execution**: When graph execution may need to be **resumed** after being **interrupted** (e.g., for **human-in-the-loop**), **tasks** can encapsulate any source of non-determinism, such as API calls, database queries, or random number generation. See the [determinism](#determinism) for more details.
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
- **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs).
## Serialization
There are two key aspects to serialization in LangGraph:
1. `@entrypoint` inputs and outputs must be JSON-serializable.
2. `@task` outputs must be JSON-serializable.
These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives
like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable.
Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution.
Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer.
## Determinism
To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic.
LangGraph achieves this behavior by persisting **task** and [**subgraph**](./low_level.md#subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same
While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them.
## Idempotency
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
## Functional API vs. Graph API
The **Functional API** and the **Graph APIs** provide two different paradigms to create workflows in LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows.
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Common Pitfalls
### Handling side effects
Side effects, such as writing to a file or sending an email, should be encapsulated in tasks to ensure consistent execution upon resumption.
=== "Incorrect"
In this example, a side effect (writing to a file) is directly included in the workflow, making resumption inconsistent.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
# This code will be executed a second time when resuming the workflow.
# Which is likely not what you want.
# highlight-next-line
with open("output.txt", "w") as f:
# highlight-next-line
f.write("Side effect executed")
value = interrupt("question")
return value
```
=== "Correct"
In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption.
```python
from langgraph.func import task
# highlight-next-line
@task
# highlight-next-line
def write_to_file():
with open("output.txt", "w") as f:
f.write("Side effect executed")
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
# The side effect is now encapsulated in a task.
write_to_file().result()
value = interrupt("question")
return value
```
### Non-deterministic control flow
[Non-deterministic control flow](#determinism) can lead to inconsistent results when resuming a workflow. To ensure correct behavior, encapsulate non-deterministic operations (e.g., random number generation, time-based logic) inside **tasks**.
=== "Incorrect"
In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed.
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
t0 = inputs["t0"]
# highlight-next-line
t1 = time.time()
delta_t = t1 - t0
if delta_t > 1:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")
return {
"result": result,
"value": value
}
```
=== "Correct"
In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input.
```python
import time
from langgraph.func import task
# highlight-next-line
@task
# highlight-next-line
def get_time() -> float:
return time.time()
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
t0 = inputs["t0"]
# highlight-next-line
t1 = get_time().result()
delta_t = t1 - t0
if delta_t > 1:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")
return {
"result": result,
"value": value
}
```
## Patterns
Below are a few simple patterns that show examples of **how to** use the **Functional API**.
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
value = inputs["value"]
another_value = inputs["another_value"]
...
my_workflow.invoke({"value": 1, "another_value": 2})
```
### Parallel execution
Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs).
```python
@task
def add_one(number: int) -> int:
return number + 1
@entrypoint(checkpointer=checkpointer)
def graph(numbers: list[int]) -> list[str]:
futures = [add_one(i) for i in numbers]
return [f.result() for f in futures]
```
### Calling subgraphs
The **Functional API** and the [**Graph API**](./low_level.md) can be used together in the same application as they share the same underlying runtime.
```python
from langgraph.func import entrypoint
from langgraph.graph import StateGraph
builder = StateGraph()
...
some_graph = builder.compile()
@entrypoint()
def some_workflow(some_input: dict) -> int:
# Call a graph defined using the graph API
result_1 = some_graph.invoke(...)
# Call another graph defined using the graph API
result_2 = another_graph.invoke(...)
return {
"result_1": result_1,
"result_2": result_2
}
```
### Calling other entrypoints
You can call other **entrypoints** from within an **entrypoint** or a **task**.
```python
@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
def some_other_workflow(inputs: dict) -> int:
return inputs["value"]
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
value = some_other_workflow.invoke({"value": 1})
return value
```
### Streaming custom data
You can stream custom data from an **entrypoint** by using the `StreamWriter` type. This allows you to write custom data to the `custom` stream.
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
@task
def add_one(x):
return x + 1
@task
def add_two(x):
return x + 2
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter) -> int:
"""A simple workflow that adds one and two to a number."""
writer("hello") # Write some data to the `custom` stream
add_one(inputs['number']).result() # Will write data to the `updates` stream
writer("world") # Write some more data to the `custom` stream
add_two(inputs['number']).result() # Will write data to the `updates` stream
return 5
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
print(chunk)
```
```pycon
('updates', {'add_one': 2})
('updates', {'add_two': 3})
('custom', 'hello')
('custom', 'world')
('updates', {'main': 5})
```
!!! important
The `writer` parameter is automatically injected at run time. It will only be injected if the
parameter name appears in the function signature with that *exact* name.
### Retry policy
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy
attempts = 0
# Let's configure the RetryPolicy to retry on ValueError.
# The default RetryPolicy is optimized for retrying specific network errors.
retry_policy = RetryPolicy(retry_on=ValueError)
@task(retry=retry_policy)
def get_info():
global attempts
attempts += 1
if attempts < 2:
raise ValueError('Failure')
return "OK"
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer):
return get_info().result()
config = {
"configurable": {
"thread_id": "1"
}
}
main.invoke({'any_input': 'foobar'}, config=config)
```
```pycon
'OK'
```
### Resuming after an error
```python
import time
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
# Global variable to track the number of attempts
attempts = 0
@task()
def get_info():
"""
Simulates a task that fails once before succeeding.
Raises an exception on the first attempt, then returns "OK" on subsequent tries.
"""
global attempts
attempts += 1
if attempts < 2:
raise ValueError("Failure") # Simulate a failure on the first attempt
return "OK"
# Initialize an in-memory checkpointer for persistence
checkpointer = MemorySaver()
@task
def slow_task():
"""
Simulates a slow-running task by introducing a 1-second delay.
"""
time.sleep(1)
return "Ran slow task."
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter):
"""
Main workflow function that runs the slow_task and get_info tasks sequentially.
Parameters:
- inputs: Dictionary containing workflow input values.
- writer: StreamWriter for streaming custom data.
The workflow first executes `slow_task` and then attempts to execute `get_info`,
which will fail on the first invocation.
"""
slow_task_result = slow_task().result() # Blocking call to slow_task
get_info().result() # Exception will be raised here on the first attempt
return slow_task_result
# Workflow execution configuration with a unique thread identifier
config = {
"configurable": {
"thread_id": "1" # Unique identifier to track workflow execution
}
}
# This invocation will take ~1 second due to the slow_task execution
try:
# First invocation will raise an exception due to the `get_info` task failing
main.invoke({'any_input': 'foobar'}, config=config)
except ValueError:
pass # Handle the failure gracefully
```
When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint.
```python
main.invoke(None, config=config)
```
```pycon
'Ran slow task.'
```
### Human-in-the-loop
The functional API supports [human-in-the-loop](human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive.
Please see the following examples for more details:
* [How to wait for user input (Functional API)](../how-tos/wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API.
* [How to review tool calls (Functional API)](../how-tos/review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API.
### Short-term memory
[State management](#state-management) using the **previous** parameter and optionally using the `entrypoint.final` primitive can be used to implement [short term memory](memory.md).
Please see the following how-to guides for more details:
* [How to add thread-level persistence (functional API)](../how-tos/persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot.
### Long-term memory
[long-term memory](memory.md#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information
about a given user in one conversation and using it in another.
Please see the following how-to guides for more details:
* [How to add cross-thread persistence (functional API)](../how-tos/cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot.
### Workflows
* [Workflows and agent](../tutorials/workflows/index.md) guide for more examples of how to build workflows using the Functional API.
### Agents
* [How to create a React agent from scratch (Functional API)](../how-tos/react-agent-from-scratch-functional.ipynb): Shows how to create a simple React agent from scratch using the functional API.
* [How to build a multi-agent network](../how-tos/multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API.
* [How to add multi-turn conversation in a multi-agent application (functional API)](../how-tos/multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents.
+1
View File
@@ -28,6 +28,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
## LangGraph Platform
@@ -0,0 +1,362 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "d2eecb96-cf0e-47ed-8116-88a7eaa4236d",
"metadata": {},
"source": [
"# How to add cross-thread persistence (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Functional API](../../concepts/functional_api/)\n",
" - [Persistence](../../concepts/persistence/)\n",
" - [Memory](../../concepts/memory/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"LangGraph allows you to persist data across **different [threads](../../concepts/persistence/#threads)**. For instance, you can store information about users (their names or preferences) in a shared (cross-thread) memory and reuse them in the new threads (e.g., new conversations).\n",
"\n",
"When using the [functional API](../../concepts/functional_api/), you can set it up to store and retrieve memories by using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface:\n",
"\n",
"1. Create an instance of a `Store`\n",
"\n",
" ```python\n",
" from langgraph.store.memory import InMemoryStore, BaseStore\n",
" \n",
" store = InMemoryStore()\n",
" ```\n",
"\n",
"2. Pass the `store` instance to the `entrypoint()` decorator and expose `store` parameter in the function signature:\n",
"\n",
" ```python\n",
" from langgraph.func import entrypoint\n",
"\n",
" @entrypoint(store=store)\n",
" def workflow(inputs: dict, store: BaseStore):\n",
" my_task(inputs).result()\n",
" ...\n",
" ```\n",
" \n",
"In this guide, we will show how to construct and use a workflow that has a shared memory implemented using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface.\n",
"\n",
"!!! note Note\n",
"\n",
" Support for the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.32`.\n",
"\n",
" Support for __index__ and __query__ arguments of the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.54`.\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need to add cross-thread persistence to a `StateGraph`, check out this [how-to guide](../cross-thread-persistence).\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3457aadf",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langchain_anthropic langchain_openai langgraph"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aa2c64a7",
"metadata": {},
"outputs": [],
"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\")\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "51b6817d",
"metadata": {},
"source": [
"!!! tip \"Set up [LangSmith](https://smith.langchain.com) for LangGraph development\"\n",
"\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com)"
]
},
{
"cell_type": "markdown",
"id": "6b5b3d42-3d2c-455e-ac10-e2ae74dc1cf1",
"metadata": {},
"source": [
"## Example: simple chatbot with long-term memory"
]
},
{
"cell_type": "markdown",
"id": "c4c550b5-1954-496b-8b9d-800361af17dc",
"metadata": {},
"source": [
"### Define store\n",
"\n",
"In this example we will create a workflow that will be able to retrieve information about a user's preferences. We will do so by defining an `InMemoryStore` - an object that can store data in memory and query that data.\n",
"\n",
"When storing objects using the `Store` interface you define two things:\n",
"\n",
"* the namespace for the object, a tuple (similar to directories)\n",
"* the object key (similar to filenames)\n",
"\n",
"In our example, we'll be using `(\"memories\", <user_id>)` as namespace and random UUID as key for each new memory.\n",
"\n",
"Importantly, to determine the user, we will be passing `user_id` via the config keyword argument of the node function.\n",
"\n",
"Let's first define our store!"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "a7f303d6-612e-4e34-bf36-29d4ed25d802",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.store.memory import InMemoryStore\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"in_memory_store = InMemoryStore(\n",
" index={\n",
" \"embed\": OpenAIEmbeddings(model=\"text-embedding-3-small\"),\n",
" \"dims\": 1536,\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"id": "3389c9f4-226d-40c7-8bfc-ee8aac24f79d",
"metadata": {},
"source": [
"### Create workflow"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2a30a362-528c-45ee-9df6-630d2d843588",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.runnables import RunnableConfig\n",
"from langchain_core.messages import BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.store.base import BaseStore\n",
"\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"\n",
"@task\n",
"def call_model(messages: list[BaseMessage], memory_store: BaseStore, user_id: str):\n",
" namespace = (\"memories\", user_id)\n",
" last_message = messages[-1]\n",
" memories = memory_store.search(namespace, query=str(last_message.content))\n",
" info = \"\\n\".join([d.value[\"data\"] for d in memories])\n",
" system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n",
"\n",
" # Store new memories if the user asks the model to remember\n",
" if \"remember\" in last_message.content.lower():\n",
" memory = \"User name is Bob\"\n",
" memory_store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
"\n",
" response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + messages)\n",
" return response\n",
"\n",
"\n",
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
"def workflow(\n",
" inputs: list[BaseMessage],\n",
" *,\n",
" previous: list[BaseMessage],\n",
" config: RunnableConfig,\n",
" store: BaseStore,\n",
"):\n",
" user_id = config[\"configurable\"][\"user_id\"]\n",
" previous = previous or []\n",
" inputs = add_messages(previous, inputs)\n",
" response = call_model(inputs, store, user_id).result()\n",
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
]
},
{
"cell_type": "markdown",
"id": "f22a4a18-67e4-4f0b-b655-a29bbe202e1c",
"metadata": {},
"source": [
"!!! note Note\n",
"\n",
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass store to the entrypoint decorator, since it's done automatically."
]
},
{
"cell_type": "markdown",
"id": "552d4e33-556d-4fa5-8094-2a076bc21529",
"metadata": {},
"source": [
"### Run the workflow!"
]
},
{
"cell_type": "markdown",
"id": "1842c626-6cd9-4f58-b549-58978e478098",
"metadata": {},
"source": [
"Now let's specify a user ID in the config and tell the model our name:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c871a073-a466-46ad-aafe-2b870831057e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Hello Bob! Nice to meet you. I'll remember that your name is Bob. How can I help you today?\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d862be40-1f8a-4057-81c4-b7bf073dc4c1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Your name is Bob.\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "80fd01ec-f135-4811-8743-daff8daea422",
"metadata": {},
"source": [
"We can now inspect our in-memory store and verify that we have in fact saved the memories for the user:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "76cde493-89cf-4709-a339-207d2b7e9ea7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'data': 'User name is Bob'}\n"
]
}
],
"source": [
"for memory in in_memory_store.search((\"memories\", \"1\")):\n",
" print(memory.value)"
]
},
{
"cell_type": "markdown",
"id": "23f5d7eb-af23-4131-b8fd-2a69e74e6e55",
"metadata": {},
"source": [
"Let's now run the workflow for another user to verify that the memories about the first user are self contained:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "d362350b-d730-48bd-9652-983812fd7811",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"I don't have any information about your name. I can only see our current conversation without any prior context or personal details about you. If you'd like me to know your name, feel free to tell me!\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -176,7 +176,7 @@
" store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
"\n",
" response = model.invoke(\n",
" [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
" [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
" )\n",
" return {\"messages\": response}\n",
"\n",
@@ -240,7 +240,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -266,7 +266,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -327,7 +327,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
+23
View File
@@ -31,6 +31,12 @@ These how-to guides show how to achieve that controllability.
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
See the below guides for how-to add persistence to your workflow using the (beta)
[Functional API](../concepts/functional_api.md):
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
### Memory
LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) in your graph. These how-to guides show how to implement different strategies for that.
@@ -59,6 +65,12 @@ Other methods:
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
- [How to review tool calls (Functional API)](review-tool-calls-functional.ipynb)
### Time Travel
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
@@ -115,6 +127,12 @@ These how-to guides show common patterns for tool calling with LangGraph:
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
See the below guides for how-to implement multi-agent workflows with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
### State Management
- [How to use Pydantic model as graph state](state-model.ipynb)
@@ -152,6 +170,11 @@ overview of its underlying implementation to help you customize for your own nee
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
See the below guide for how-to build ReAct agents with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
## LangGraph Platform
This section includes how-to guides for LangGraph Platform.
@@ -0,0 +1,449 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "a2b182eb-1e31-43c8-85b1-706508dfa370",
"metadata": {},
"source": [
"# How to add multi-turn conversation in a multi-agent application (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - [Multi-agent systems](../../concepts/multi_agent)\n",
" - [Human-in-the-loop](../../concepts/human_in_the_loop)\n",
" - [Functional API](../../concepts/functional_api)\n",
" - [Command](../../concepts/low_level/#command)\n",
" - [LangGraph Glossary](../../concepts/low_level/)\n",
"\n",
"\n",
"In this how-to guide, we’ll build an application that allows an end-user to engage in a *multi-turn conversation* with one or more agents. We'll create a node that uses an [`interrupt`](../../reference/types/#langgraph.types.interrupt) to collect user input and routes back to the **active** agent.\n",
"\n",
"The agents will be implemented as tasks in a workflow that executes agent steps and determines the next action:\n",
"\n",
"1. **Wait for user input** to continue the conversation, or\n",
"2. **Route to another agent** (or back to itself, such as in a loop) via a [**handoff**](../../concepts/multi_agent/#handoffs).\n",
"\n",
"```python\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_core.tools import tool\n",
"from langgraph.types import interrupt\n",
"\n",
"\n",
"# Define a tool to signal intent to hand off to a different agent\n",
"# Note: this is not using Command(goto) syntax for navigating to different agents:\n",
"# `workflow()` below handles the handoffs explicitly\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"# define an agent\n",
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
"\n",
"\n",
"# define a task that calls an agent\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# define the multi-agent network workflow\n",
"@entrypoint(checkpointer)\n",
"def workflow(messages):\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" ai_msg = get_last_ai_msg(agent_messages)\n",
" if not ai_msg.tool_calls:\n",
" user_input = interrupt(value=\"Ready for user input.\")\n",
" messages = messages + [{\"role\": \"user\", \"content\": user_input}]\n",
" continue\n",
"\n",
" messages = messages + agent_messages\n",
" call_active_agent = get_next_agent(messages)\n",
" return entrypoint.final(value=agent_messages[-1], save=messages)\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"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",
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "c217c3fe-ca50-45a1-be91-912bc83ed8b3",
"metadata": {},
"source": [
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
"\n",
"We will create 2 agents:\n",
"\n",
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
"\n",
"This is a fully-connected network - every agent can talk to any other agent. "
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "eb51463a-4425-44ad-91d5-f21fd5b4e3b3",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"from typing_extensions import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_travel_recommendations():\n",
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
"\n",
"\n",
"@tool\n",
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
" return {\n",
" \"aruba\": [\n",
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
" ],\n",
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
" }[location]\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_travel_advisor():\n",
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to travel advisor\""
]
},
{
"cell_type": "markdown",
"id": "7f5b2a7f",
"metadata": {},
"source": [
"!!! note \"Transfer tools\"\n",
"\n",
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
" \n",
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
]
},
{
"cell_type": "markdown",
"id": "213d661e-6ba4-42b9-bc7f-6c8c423e3419",
"metadata": {},
"source": [
"Let's now create our agents using the the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] and our multi-agent workflow. Note that will be calling [`interrupt`][langgraph.types.interrupt] every time after we get the final response from each of the agents."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "aa4bdbff-9461-46cc-aee9-8a22d3c3d9ec",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.types import interrupt, Command\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"# Define travel advisor ReAct agent\n",
"travel_advisor_tools = [\n",
" get_travel_recommendations,\n",
" transfer_to_hotel_advisor,\n",
"]\n",
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\n",
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# Define hotel advisor ReAct agent\n",
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\n",
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_hotel_advisor(messages):\n",
" response = hotel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def multi_turn_graph(messages, previous):\n",
" previous = previous or []\n",
" messages = add_messages(previous, messages)\n",
"\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = add_messages(messages, agent_messages)\n",
" # Find the last AI message\n",
" # If one of the handoff tools is called, the last message returned\n",
" # by the agent will be a ToolMessage because we set them to have\n",
" # \"return_direct=True\". This means that the last AIMessage will\n",
" # have tool calls.\n",
" # Otherwise, the last returned message will be an AIMessage with\n",
" # no tool calls, which means we are ready for new input.\n",
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
" if not ai_msg.tool_calls:\n",
" user_input = interrupt(value=\"Ready for user input.\")\n",
" messages = add_messages(messages, [{\"role\": \"user\", \"content\": user_input}])\n",
" continue\n",
"\n",
" tool_call = ai_msg.tool_calls[-1]\n",
" if tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
" call_active_agent = call_hotel_advisor\n",
" elif tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
" call_active_agent = call_travel_advisor\n",
" else:\n",
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
"\n",
" return entrypoint.final(value=agent_messages[-1], save=messages)"
]
},
{
"cell_type": "markdown",
"id": "af856e1b-41fc-4041-8cbf-3818a60088e0",
"metadata": {},
"source": [
"## Test multi-turn conversation\n",
"\n",
"Let's test a multi turn conversation with this application."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "161e0cf1-d13a-4026-8f89-bdab67d1ad4d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"--- Conversation Turn 1 ---\n",
"\n",
"User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}\n",
"\n",
"call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known for its perfect warm weather year-round, with consistent temperatures around 82°F (28°C) and very little rainfall. The island offers:\n",
"\n",
"1. Beautiful white-sand beaches like Eagle Beach and Palm Beach\n",
"2. Crystal clear waters perfect for swimming and snorkeling\n",
"3. Constant cooling trade winds that make the warm weather comfortable\n",
"4. A mix of luxury resorts and boutique hotels\n",
"5. Diverse activities from water sports to desert-like terrain exploration\n",
"6. Great dining and nightlife options\n",
"7. Safe and tourist-friendly environment\n",
"\n",
"Would you like me to connect you with our hotel advisor to help you find the perfect place to stay in Aruba?\n",
"\n",
"--- Conversation Turn 2 ---\n",
"\n",
"User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')\n",
"\n",
"call_hotel_advisor: Based on the recommendations, I can highlight two excellent options in different areas:\n",
"\n",
"1. The Ritz-Carlton, Aruba - Located in Palm Beach\n",
"- Part of the high-rise hotel district\n",
"- Luxury beachfront resort with full-service spa\n",
"- Multiple restaurants and a casino\n",
"- Perfect for those who want to be in the heart of the action\n",
"- Close to shopping, dining, and nightlife\n",
"\n",
"2. Bucuti & Tara Beach Resort - Located in Eagle Beach\n",
"- Adults-only boutique resort\n",
"- Located on Eagle Beach, voted one of the best beaches in the world\n",
"- More serene and romantic atmosphere\n",
"- Perfect for couples and those seeking a quieter vacation\n",
"- Known for its excellent service and sustainability practices\n",
"\n",
"Would you like more specific information about either of these properties or would you like to explore other options in either area?\n",
"\n",
"--- Conversation Turn 3 ---\n",
"\n",
"User: Command(resume='i like the first one. could you recommend something to do near the hotel?')\n",
"\n",
"call_travel_advisor: Near the Ritz-Carlton in Palm Beach, you can enjoy several fantastic activities:\n",
"\n",
"1. Paseo Herencia Mall - A beautiful outdoor shopping and entertainment center just a short walk away\n",
"2. High-Rise Beach Strip - Perfect for beach walks and water sports\n",
"3. Bubali Bird Sanctuary - A nature preserve where you can spot local wildlife\n",
"4. The Butterfly Farm - A unique attraction featuring hundreds of exotic butterflies\n",
"5. Palm Beach Plaza Mall - Great for shopping and dining\n",
"6. Various water sports operators offering:\n",
" - Jet skiing\n",
" - Parasailing\n",
" - Snorkeling trips\n",
" - Sunset sailing cruises\n",
"\n",
"Additionally, the hotel concierge can arrange most activities directly for you. Would you like more specific information about any of these activities?\n"
]
}
],
"source": [
"import uuid\n",
"\n",
"thread_config = {\"configurable\": {\"thread_id\": uuid.uuid4()}}\n",
"\n",
"inputs = [\n",
" # 1st round of conversation,\n",
" {\"role\": \"user\", \"content\": \"i wanna go somewhere warm in the caribbean\"},\n",
" # Since we're using `interrupt`, we'll need to resume using the Command primitive.\n",
" # 2nd round of conversation,\n",
" Command(\n",
" resume=\"could you recommend a nice hotel in one of the areas and tell me which area it is.\"\n",
" ),\n",
" # 3rd round of conversation,\n",
" Command(\n",
" resume=\"i like the first one. could you recommend something to do near the hotel?\"\n",
" ),\n",
"]\n",
"\n",
"for idx, user_input in enumerate(inputs):\n",
" print()\n",
" print(f\"--- Conversation Turn {idx + 1} ---\")\n",
" print()\n",
" print(f\"User: {user_input}\")\n",
" print()\n",
" for update in multi_turn_graph.stream(\n",
" user_input,\n",
" config=thread_config,\n",
" stream_mode=\"updates\",\n",
" ):\n",
" for node_id, value in update.items():\n",
" if isinstance(value, list) and value:\n",
" last_message = value[-1]\n",
" if isinstance(last_message, dict) or last_message.type != \"ai\":\n",
" continue\n",
" print(f\"{node_id}: {last_message.content}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,500 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "87684b48-150e-4e15-b0a5-a9dd7851f8fb",
"metadata": {},
"source": [
"# How to build a multi-agent network (functional API)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2c65639c-9705-49f1-840a-370718852e98",
"metadata": {},
"source": [
"!!! info \"Prerequisites\" \n",
" This guide assumes familiarity with the following:\n",
"\n",
" - [Multi-agent systems](../../concepts/multi_agent)\n",
" - [Functional API](../../concepts/functional_api)\n",
" - [Command](../../concepts/low_level/#command)\n",
" - [LangGraph Glossary](../../concepts/low_level/)\n",
"\n",
"In this how-to guide we will demonstrate how to implement a [multi-agent network](../../concepts/multi_agent#network) architecture where each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. We will be using [functional API](../../concepts/functional_api) — individual agents will be defined as tasks and the agent handoffs will be defined in the main [entrypoint()][langgraph.func.entrypoint]:\n",
"\n",
"```python\n",
"from langgraph.func import entrypoint\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"# Define a tool to signal intent to hand off to a different agent\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"# define an agent\n",
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
"\n",
"\n",
"# define a task that calls an agent\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# define the multi-agent network workflow\n",
"@entrypoint()\n",
"def workflow(messages):\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = messages + agent_messages\n",
" call_active_agent = get_next_agent(messages)\n",
" return messages\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"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",
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "4a53f304-3709-4df7-8714-1ca61e615743",
"metadata": {},
"source": [
"## Travel agent example"
]
},
{
"cell_type": "markdown",
"id": "34cd131b-f0c2-4b69-887f-2cbd5afb14a7",
"metadata": {},
"source": [
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
"\n",
"We will create 2 agents:\n",
"\n",
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
"\n",
"This is a fully-connected network - every agent can talk to any other agent. "
]
},
{
"cell_type": "markdown",
"id": "fedc9ed0-e90c-4ee1-a7c6-f5af3c634a7b",
"metadata": {},
"source": [
"First, let's create some of the tools that the agents will be using:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7e31f258-ec28-4020-b86d-c91dfa9a3bfc",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"from typing_extensions import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_travel_recommendations():\n",
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
"\n",
"\n",
"@tool\n",
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
" return {\n",
" \"aruba\": [\n",
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
" ],\n",
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
" }[location]\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_travel_advisor():\n",
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to travel advisor\""
]
},
{
"cell_type": "markdown",
"id": "d8519a32-d23b-48b0-bd18-74f8c0dacf58",
"metadata": {},
"source": [
"!!! note \"Transfer tools\"\n",
"\n",
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
" \n",
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
]
},
{
"cell_type": "markdown",
"id": "93dbc3bd-27b9-4d79-b5dd-be592bc50f74",
"metadata": {},
"source": [
"Now let's define our agent tasks and combine them into a single multi-agent network workflow:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "b638d6c4-3de6-4921-980c-2df1bd1cc9c7",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"# Define travel advisor ReAct agent\n",
"travel_advisor_tools = [\n",
" get_travel_recommendations,\n",
" transfer_to_hotel_advisor,\n",
"]\n",
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\n",
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# Define hotel advisor ReAct agent\n",
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\n",
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_hotel_advisor(messages):\n",
" response = hotel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"@entrypoint()\n",
"def workflow(messages):\n",
" messages = add_messages([], messages)\n",
"\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = add_messages(messages, agent_messages)\n",
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
" if not ai_msg.tool_calls:\n",
" break\n",
"\n",
" tool_call = ai_msg.tool_calls[-1]\n",
" if tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
" call_active_agent = call_travel_advisor\n",
" elif tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
" call_active_agent = call_hotel_advisor\n",
" else:\n",
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
"\n",
" return messages"
]
},
{
"cell_type": "markdown",
"id": "9223db83-1938-434a-9d24-8666842a8eea",
"metadata": {},
"source": [
"Lastly, let's define a helper to render the agent outputs:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "058f3d96-534f-4b97-afb3-799ba81224ea",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import convert_to_messages\n",
"\n",
"\n",
"def pretty_print_messages(update):\n",
" if isinstance(update, tuple):\n",
" ns, update = update\n",
" # skip parent graph updates in the printouts\n",
" if len(ns) == 0:\n",
" return\n",
"\n",
" graph_id = ns[-1].split(\":\")[0]\n",
" print(f\"Update from subgraph {graph_id}:\")\n",
" print(\"\\n\")\n",
"\n",
" for node_name, node_update in update.items():\n",
" print(f\"Update from node {node_name}:\")\n",
" print(\"\\n\")\n",
"\n",
" for m in convert_to_messages(node_update[\"messages\"]):\n",
" m.pretty_print()\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "7132e2c0-d937-4325-a30e-e715c5304fe0",
"metadata": {},
"source": [
"Let's test it out using the same input as our original multi-agent system:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "29b47c57-ad05-4f10-83bf-c3ff6ff8eb93",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"I'll help you find a warm Caribbean destination and then get some hotel recommendations for you.\\n\\nLet me first get some destination recommendations for the Caribbean region.\", 'type': 'text'}, {'id': 'toolu_015vT8PkPq1VXvjrDvSpWUwJ', 'input': {}, 'name': 'get_travel_recommendations', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" get_travel_recommendations (toolu_015vT8PkPq1VXvjrDvSpWUwJ)\n",
" Call ID: toolu_015vT8PkPq1VXvjrDvSpWUwJ\n",
" Args:\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_travel_recommendations\n",
"\n",
"turks and caicos\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Based on the recommendation, I suggest Turks and Caicos! This beautiful British Overseas Territory is known for its stunning white-sand beaches, crystal-clear turquoise waters, and year-round warm weather. Grace Bay Beach in Providenciales is consistently ranked among the world's best beaches. The islands offer excellent snorkeling, diving, and water sports opportunities, plus a relaxed Caribbean atmosphere.\\n\\nNow, let me connect you with our hotel advisor to get some specific hotel recommendations for Turks and Caicos.\", 'type': 'text'}, {'id': 'toolu_01JY7pNNWFuaWoe9ymxFYiPV', 'input': {}, 'name': 'transfer_to_hotel_advisor', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" transfer_to_hotel_advisor (toolu_01JY7pNNWFuaWoe9ymxFYiPV)\n",
" Call ID: toolu_01JY7pNNWFuaWoe9ymxFYiPV\n",
" Args:\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: transfer_to_hotel_advisor\n",
"\n",
"Successfully transferred to hotel advisor\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': 'Let me get some hotel recommendations for Turks and Caicos:', 'type': 'text'}, {'id': 'toolu_0129ELa7jFocn16bowaGNapg', 'input': {'location': 'turks and caicos'}, 'name': 'get_hotel_recommendations', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" get_hotel_recommendations (toolu_0129ELa7jFocn16bowaGNapg)\n",
" Call ID: toolu_0129ELa7jFocn16bowaGNapg\n",
" Args:\n",
" location: turks and caicos\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_hotel_recommendations\n",
"\n",
"[\"Grace Bay Club\", \"COMO Parrot Cay\"]\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Here are two excellent hotel options in Turks and Caicos:\n",
"\n",
"1. Grace Bay Club: This luxury resort is located on the world-famous Grace Bay Beach. It offers all-oceanfront suites, exceptional dining options, and personalized service. The resort features adult-only and family-friendly sections, making it perfect for any type of traveler.\n",
"\n",
"2. COMO Parrot Cay: This exclusive private island resort offers the ultimate luxury escape. It's known for its pristine beach, world-class spa, and holistic wellness programs. The resort provides an intimate, secluded experience with top-notch amenities and service.\n",
"\n",
"Would you like more specific information about either of these properties or would you like to explore hotels in another destination?\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in workflow.stream(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"i wanna go somewhere warm in the caribbean. pick one destination and give me hotel recommendations\",\n",
" }\n",
" ],\n",
" subgraphs=True,\n",
"):\n",
" pretty_print_messages(chunk)"
]
},
{
"cell_type": "markdown",
"id": "d7d89ee0-0229-4718-9b98-bdd3f59c1014",
"metadata": {},
"source": [
"Voila - `travel_advisor` picks a destination and then makes a decision to call `hotel_advisor` for more info!"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,349 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to add thread-level persistence (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Functional API](../../concepts/functional_api/)\n",
" - [Persistence](../../concepts/persistence/)\n",
" - [Memory](../../concepts/memory/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"Many AI applications need memory to share context across multiple interactions on the same [thread](../../concepts/persistence#threads) (e.g., multiple turns of a conversation). In LangGraph functional API, this kind of memory can be added to any [entrypoint()][langgraph.func.entrypoint] workflow using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence).\n",
"\n",
"When creating a LangGraph workflow, you can set it up to persist its results by using a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver):\n",
"\n",
"\n",
"1. Create an instance of a checkpointer:\n",
"\n",
" ```python\n",
" from langgraph.checkpoint.memory import MemorySaver\n",
" \n",
" checkpointer = MemorySaver() \n",
" ```\n",
"\n",
"2. Pass `checkpointer` instance to the `entrypoint()` decorator:\n",
"\n",
" ```python\n",
" from langgraph.func import entrypoint\n",
" \n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(inputs)\n",
" ...\n",
" ```\n",
"\n",
"3. Optionally expose `previous` parameter in the workflow function signature:\n",
"\n",
" ```python\n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(\n",
" inputs,\n",
" *,\n",
" # you can optionally specify `previous` in the workflow function signature\n",
" # to access the return value from the workflow as of the last execution\n",
" previous\n",
" ):\n",
" previous = previous or []\n",
" combined_inputs = previous + inputs\n",
" result = do_something(combined_inputs)\n",
" ...\n",
" ```\n",
"\n",
"4. Optionally choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous`:\n",
"\n",
" ```python\n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(inputs, *, previous):\n",
" ...\n",
" result = do_something(...)\n",
" return entrypoint.final(value=result, save=combine(inputs, result))\n",
" ```\n",
"\n",
"This guide shows how you can add thread-level persistence to your workflow.\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need memory that is __shared__ across multiple conversations or users (cross-thread persistence), check out this [how-to guide](../cross-thread-persistence-functional).\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need to add thread-level persistence to a `StateGraph`, check out this [how-to guide](../persistence)."
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API key for Anthropic (the LLM we will use)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"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",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "4cf509bc",
"metadata": {},
"source": [
"## Example: simple chatbot with short-term memory\n",
"\n",
"We will be using a workflow with a single task that calls a [chat model](https://python.langchain.com/docs/concepts/chat_models/).\n",
"\n",
"Let's first define the model we'll be using:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")"
]
},
{
"cell_type": "markdown",
"id": "7b7a2792-982b-4e47-83eb-0c594725d1c1",
"metadata": {},
"source": [
"Now we can define our task and workflow. To add in persistence, we need to pass in a [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.base.BaseCheckpointSaver) to the [entrypoint()][langgraph.func.entrypoint] decorator."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "87326ea6-34c5-46da-a41f-dda26ef9bd74",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import BaseMessage\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@task\n",
"def call_model(messages: list[BaseMessage]):\n",
" response = model.invoke(messages)\n",
" return response\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]):\n",
" if previous:\n",
" inputs = add_messages(previous, inputs)\n",
"\n",
" response = call_model(inputs).result()\n",
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
]
},
{
"cell_type": "markdown",
"id": "250d8fd9-2e7a-4892-9adc-19762a1e3cce",
"metadata": {},
"source": [
"If we try to use this workflow, the context of the conversation will be persisted across interactions:"
]
},
{
"cell_type": "markdown",
"id": "7654ebcc-2179-41b4-92d1-6666f6f8634f",
"metadata": {},
"source": [
"!!! note Note\n",
"\n",
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass checkpointer to the entrypoint decorator, since it's done automatically."
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"We can now interact with the agent and see that it remembers previous messages!"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Hi Bob! I'm Claude. Nice to meet you! How are you today?\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "1bb07bf8-68b7-4049-a0f1-eb67a4879a3a",
"metadata": {},
"source": [
"You can always resume previous threads:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Your name is Bob.\n"
]
}
],
"source": [
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "3f47bbfc-d9ef-4288-ba4a-ebbc0136fa9d",
"metadata": {},
"source": [
"If we want to start a new conversation, we can pass in a different `thread_id`. Poof! All the memories are gone!"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "273d56a8-f40f-4a51-a27f-7c6bb2bda0ba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"I don't know your name unless you tell me. Each conversation I have starts fresh, so I don't have access to any previous interactions or personal information unless you share it with me.\n"
]
}
],
"source": [
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in workflow.stream(\n",
" [input_message],\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "ac7926a8-4c88-4b16-973c-53d6da3f4a08",
"metadata": {},
"source": [
"!!! tip \"Streaming tokens\"\n",
"\n",
" If you would like to stream LLM tokens from your chatbot, you can use `stream_mode=\"messages\"`. Check out this [how-to guide](../streaming-tokens) to learn more."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+5 -5
View File
@@ -211,11 +211,11 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()\n",
"\n",
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -286,7 +286,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -319,7 +319,7 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -352,7 +352,7 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
@@ -0,0 +1,463 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to create a ReAct agent from scratch (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models)\n",
" - [Messages](https://python.langchain.com/docs/concepts/messages)\n",
" - [Tool Calling](https://python.langchain.com/docs/concepts/tool_calling/)\n",
" - [Entrypoints](../../concepts/functional_api/#entrypoint) and [Tasks](../../concepts/functional_api/#task)\n",
"\n",
"This guide demonstrates how to implement a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n",
"\n",
"The ReAct agent is a [tool-calling agent](../../concepts/agentic_concepts/#tool-calling-agent) that operates as follows:\n",
"\n",
"1. Queries are issued to a chat model;\n",
"2. If the model generates no [tool calls](../../concepts/agentic_concepts/#tool-calling), we return the model response.\n",
"3. If the model generates tool calls, we execute the tool calls with available tools, append them as [tool messages](https://python.langchain.com/docs/concepts/messages/) to our message list, and repeat the process.\n",
"\n",
"This is a simple and versatile set-up that can be extended with memory, human-in-the-loop capabilities, and other features. See the dedicated [how-to guides](../../how-tos/#prebuilt-react-agent) for examples.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"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(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create ReAct agent\n",
"\n",
"Now that you have installed the required packages and set your environment variables, we can create our agent.\n",
"\n",
"### Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. Here we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\"\n",
"\n",
"\n",
"tools = [get_weather]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define tasks\n",
"\n",
"We next define the [tasks](../../concepts/functional_api/#task) we will execute. Here there are two different tasks:\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define entrypoint\n",
"\n",
"Our [entrypoint](../../concepts/functional_api/#entrypoint) will handle the orchestration of these two tasks. As described above, when our `call_model` task generates tool calls, the `call_tool` task will generate responses for each. We append all messages to a single messages list.\n",
"\n",
"!!! tip\n",
" Note that because tasks return future-like objects, the below implementation executes tools in parallel."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"@entrypoint()\n",
"def agent(messages):\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" return llm_response"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"To use our agent, we invoke it with a messages list. Based on our implementation, these can be LangChain [message](https://python.langchain.com/docs/concepts/messages/) objects or OpenAI-style dicts:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_tNnkrjnoz6MNfCHJpwfuEQ0v)\n",
" Call ID: call_tNnkrjnoz6MNfCHJpwfuEQ0v\n",
" Args:\n",
" location: san francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message]):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Perfect! The graph correctly calls the `get_weather` tool and responds to the user after receiving the information from the tool. Check out the LangSmith trace [here](https://smith.langchain.com/public/d5a0d5ea-bdaa-4032-911e-7db177c8141b/r)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Add thread-level persistence\n",
"\n",
"Adding [thread-level persistence](../../concepts/persistence#threads) lets us support conversational experiences with our agent: subsequent invocations will append to the prior messages list, retaining the full conversational context.\n",
"\n",
"To add thread-level persistence to our agent:\n",
"\n",
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [MemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.MemorySaver), a simple in-memory checkpointer.\n",
"2. Update our entrypoint to accept the previous messages state as a second argument. Here, we simply append the message updates to the previous sequence of messages.\n",
"3. Choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous` using `entrypoint.final` (optional)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"# highlight-next-line\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"# highlight-next-line\n",
"@entrypoint(checkpointer=checkpointer)\n",
"# highlight-next-line\n",
"def agent(messages, previous):\n",
" # highlight-next-line\n",
" if previous is not None:\n",
" # highlight-next-line\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" # highlight-next-line\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will now need to pass in a config when running our application. The config will specify an identifier for the conversational thread.\n",
"\n",
"!!! tip\n",
"\n",
" Read more about thread-level persistence in our [concepts page](../../concepts/persistence/) and [how-to guides](../../how-tos/#persistence)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We start a thread the same way as before, this time passing in the config:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_lubbUSdDofmOhFunPEZLBz3g)\n",
" Call ID: call_lubbUSdDofmOhFunPEZLBz3g\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"# highlight-next-line\n",
"for step in agent.stream([user_message], config):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When we ask a follow-up conversation, the model uses the prior context to infer that we are asking about the weather:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': 'How does it compare to Boston, MA?'}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_8sTKYAhSIHOdjLD5d6gaswuV)\n",
" Call ID: call_8sTKYAhSIHOdjLD5d6gaswuV\n",
" Args:\n",
" location: Boston, MA\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's rainy!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Compared to San Francisco, which is sunny, Boston, MA is experiencing rainy weather.\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"How does it compare to Boston, MA?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the [LangSmith trace](https://smith.langchain.com/public/20a1116b-bb3b-44c1-8765-7a28663439d9/r), we can see that the full conversational context is retained in each model call."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,627 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to review tool calls (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n",
" - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n",
"\n",
"This guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n",
"\n",
"We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n",
"\n",
"Specifically, we will demonstrate how to review [tool calls](https://python.langchain.com/docs/concepts/tool_calling/) generated by a [chat model](https://python.langchain.com/docs/concepts/chat_models/) prior to their execution. This can be accomplished through use of the [interrupt](../../concepts/human_in_the_loop/#interrupt) function at key points in our application.\n",
"\n",
"**Preview**:\n",
"\n",
"We will implement a simple function that reviews tool calls generated from our chat model and call it from inside our application's [entrypoint](../../concepts/functional_api/#entrypoint):\n",
"\n",
"```python\n",
"def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n",
" \"\"\"Review a tool call, returning a validated version.\"\"\"\n",
" human_review = interrupt(\n",
" {\n",
" \"question\": \"Is this correct?\",\n",
" \"tool_call\": tool_call,\n",
" }\n",
" )\n",
" review_action = human_review[\"action\"]\n",
" review_data = human_review.get(\"data\")\n",
" if review_action == \"continue\":\n",
" return tool_call\n",
" elif review_action == \"update\":\n",
" updated_tool_call = {**tool_call, **{\"args\": review_data}}\n",
" return updated_tool_call\n",
" elif review_action == \"feedback\":\n",
" return ToolMessage(\n",
" content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n",
" )\n",
"```\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"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(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\"\n",
"\n",
"\n",
"tools = [get_weather]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define tasks\n",
"\n",
"Our [tasks](../../concepts/functional_api/#task) are unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolCall, ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define entrypoint\n",
"\n",
"To review tool calls before execution, we add a `review_tool_call` function that calls [interrupt](../../concepts/human_in_the_loop/#interrupt). When this function is called, execution will be paused until we issue a command to resume it.\n",
"\n",
"Given a tool call, our function will `interrupt` for human review. At that point we can either:\n",
"\n",
"- Accept the tool call;\n",
"- Revise the tool call and continue;\n",
"- Generate a custom tool message (e.g., instructing the model to re-format its tool call).\n",
"\n",
"We will demonstrate these three cases in the [usage examples](#usage) below."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from typing import Union\n",
"\n",
"\n",
"def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n",
" \"\"\"Review a tool call, returning a validated version.\"\"\"\n",
" human_review = interrupt(\n",
" {\n",
" \"question\": \"Is this correct?\",\n",
" \"tool_call\": tool_call,\n",
" }\n",
" )\n",
" review_action = human_review[\"action\"]\n",
" review_data = human_review.get(\"data\")\n",
" if review_action == \"continue\":\n",
" return tool_call\n",
" elif review_action == \"update\":\n",
" updated_tool_call = {**tool_call, **{\"args\": review_data}}\n",
" return updated_tool_call\n",
" elif review_action == \"feedback\":\n",
" return ToolMessage(\n",
" content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now update our [entrypoint](../../concepts/functional_api/#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human.\n",
"\n",
"!!! tip\n",
"\n",
" The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def agent(messages, previous):\n",
" if previous is not None:\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Review tool calls\n",
" tool_results = []\n",
" tool_calls = []\n",
" for i, tool_call in enumerate(llm_response.tool_calls):\n",
" review = review_tool_call(tool_call)\n",
" if isinstance(review, ToolMessage):\n",
" tool_results.append(review)\n",
" else: # is a validated tool call\n",
" tool_calls.append(review)\n",
" if review != tool_call:\n",
" llm_response.tool_calls[i] = review # update message\n",
"\n",
" # Execute remaining tool calls\n",
" tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls]\n",
" remaining_tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(\n",
" messages,\n",
" [llm_response, *tool_results, *remaining_tool_results],\n",
" )\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Usage\n",
"\n",
"Let's demonstrate some scenarios."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def _print_step(step: dict) -> None:\n",
" for task_name, result in step.items():\n",
" if task_name == \"agent\":\n",
" continue # just stream from tasks\n",
" print(f\"\\n{task_name}:\")\n",
" if task_name in (\"__interrupt__\", \"review_tool_call\"):\n",
" print(result)\n",
" else:\n",
" result.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Accept a tool call\n",
"\n",
"To accept a tool call, we just indicate in the data we provide in the `Command` that the tool call should pass through."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_Bh5cSwMqCpCxTjx7AjdrQTPd)\n",
" Call ID: call_Bh5cSwMqCpCxTjx7AjdrQTPd\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_Bh5cSwMqCpCxTjx7AjdrQTPd', 'type': 'tool_call'}}, resumable=True, ns=['agent:22fcc9cd-3573-b39b-eea7-272a025903e2'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"continue\"})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Revise a tool call\n",
"\n",
"To revise a tool call, we can supply updated arguments."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\"}}"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_b9h8e18FqH0IQm3NMoeYKz6N)\n",
" Call ID: call_b9h8e18FqH0IQm3NMoeYKz6N\n",
" Args:\n",
" location: san francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'san francisco'}, 'id': 'call_b9h8e18FqH0IQm3NMoeYKz6N', 'type': 'tool_call'}}, resumable=True, ns=['agent:9559a81d-5720-dc19-a457-457bac7bdd83'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"update\", \"data\": {\"location\": \"SF, CA\"}})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The LangSmith traces for this run are particularly informative:\n",
"\n",
"- In the trace [before the interrupt](https://smith.langchain.com/public/c8b07579-5cf4-4adb-a849-282163bc9d99/r/b5b128d6-e715-480b-b58d-59e64f724275), we generate a tool call for location `\"San Francisco\"`.\n",
"- In the trace [after resuming](https://smith.langchain.com/public/b28b92e5-a555-482d-aa4d-c675a19f0eb5/r), we see that the tool call in the message has been updated to `\"SF, CA\"`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Generate a custom ToolMessage\n",
"\n",
"To Generate a custom `ToolMessage`, we supply the content of the message. In this case we will ask the model to reformat its tool call."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\"}}"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_VqGjKE7uu8HdWs9XuY1kMV18)\n",
" Call ID: call_VqGjKE7uu8HdWs9XuY1kMV18\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_VqGjKE7uu8HdWs9XuY1kMV18', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_xoXkK8Cz0zIpvWs78qnXpvYp)\n",
" Call ID: call_xoXkK8Cz0zIpvWs78qnXpvYp\n",
" Args:\n",
" location: San Francisco, CA\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco, CA'}, 'id': 'call_xoXkK8Cz0zIpvWs78qnXpvYp', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(\n",
" # highlight-next-line\n",
" resume={\n",
" # highlight-next-line\n",
" \"action\": \"feedback\",\n",
" # highlight-next-line\n",
" \"data\": \"Please format as <City>, <State>.\",\n",
" # highlight-next-line\n",
" },\n",
" # highlight-next-line\n",
")\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once it is re-formatted, we can accept it:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco, CA is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"continue\"})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,561 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to wait for user input (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n",
" - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n",
"\n",
"**Human-in-the-loop (HIL)** interactions are crucial for [agentic systems](../../concepts/agentic_concepts/#human-in-the-loop). Waiting for human input is a common HIL interaction pattern, allowing the agent to ask the user clarifying questions and await input before proceeding. \n",
"\n",
"We can implement this in LangGraph using the [interrupt()][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input.\n",
"\n",
"This guide demonstrates how to implement human-in-the-loop workflows using LangGraph's [Functional API](../../concepts/functional_api). Specifically, we will demonstrate:\n",
"\n",
"1. [A simple usage example](#simple-usage)\n",
"2. [How to use with a ReAct agent](#agent)\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"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(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Simple usage\n",
"\n",
"Let's demonstrate a simple usage example. We will create three [tasks](../../concepts/functional_api/#task):\n",
"\n",
"1. Append `\"bar\"`.\n",
"2. Pause for human input. When resuming, append human input.\n",
"3. Append `\"qux\"`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.func import entrypoint, task\n",
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"@task\n",
"def step_1(input_query):\n",
" \"\"\"Append bar.\"\"\"\n",
" return f\"{input_query} bar\"\n",
"\n",
"\n",
"@task\n",
"def human_feedback(input_query):\n",
" \"\"\"Append user input.\"\"\"\n",
" feedback = interrupt(f\"Please provide feedback: {input_query}\")\n",
" return f\"{input_query} {feedback}\"\n",
"\n",
"\n",
"@task\n",
"def step_3(input_query):\n",
" \"\"\"Append qux.\"\"\"\n",
" return f\"{input_query} qux\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now compose these tasks in a simple [entrypoint](../../concepts/functional_api/#entrypoint):"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def graph(input_query):\n",
" result_1 = step_1(input_query).result()\n",
" result_2 = human_feedback(result_1).result()\n",
" result_3 = step_3(result_2).result()\n",
"\n",
" return result_3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"All we have done to enable human-in-the-loop workflows is called [interrupt()](../../concepts/human_in_the_loop/#interrupt) inside a task.\n",
"\n",
"!!! tip\n",
"\n",
" The results of prior tasks-- in this case `step_1`-- are persisted, so that they are not run again following the `interrupt`.\n",
"\n",
"\n",
"Let's send in a query string:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'step_1': 'foo bar'}\n",
"\n",
"\n",
"{'__interrupt__': (Interrupt(value='Please provide feedback: foo bar', resumable=True, ns=['graph:d66b2e35-0ee3-d8d6-1a22-aec9d58f13b9', 'human_feedback:e0cd4ee2-b874-e1d2-8bc4-3f7ddc06bcc2'], when='during'),)}\n",
"\n",
"\n"
]
}
],
"source": [
"for event in graph.stream(\"foo\", config):\n",
" print(event)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that we've paused with an `interrupt` after `step_1`. The interrupt provides instructions to resume the run. To resume, we issue a [Command](../../concepts/human_in_the_loop/#the-command-primitive) containing the data expected by the `human_feedback` task."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'human_feedback': 'foo bar baz'}\n",
"\n",
"\n",
"{'step_3': 'foo bar baz qux'}\n",
"\n",
"\n",
"{'graph': 'foo bar baz qux'}\n",
"\n",
"\n"
]
}
],
"source": [
"# Continue execution\n",
"for event in graph.stream(Command(resume=\"baz\"), config):\n",
" print(event)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"After resuming, the run proceeds through the remaining step and terminates as expected."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Agent\n",
"\n",
"We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n",
"\n",
"Here we will extend the agent by allowing it to reach out to a human for assistance when needed.\n",
"\n",
"### Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To reach out to a human for assistance, we can simply add a tool that calls [interrupt](../../concepts/human_in_the_loop/#interrupt):"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"@tool\n",
"def human_assistance(query: str) -> str:\n",
" \"\"\"Request assistance from a human.\"\"\"\n",
" human_response = interrupt({\"query\": query})\n",
" return human_response[\"data\"]\n",
"\n",
"\n",
"tools = [get_weather, human_assistance]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define tasks\n",
"\n",
"Our tasks are otherwise unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them.\n",
"\n",
"We just have one more tool accessible to the model."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call)\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define entrypoint\n",
"\n",
"Our [entrypoint](../../concepts/functional_api/#entrypoint) is also unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def agent(messages, previous):\n",
" if previous is not None:\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Usage\n",
"\n",
"Let's invoke our model with a question that requires human assistance. Our question will also require an invocation of the `get_weather` tool:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"def _print_step(step: dict) -> None:\n",
" for task_name, result in step.items():\n",
" if task_name == \"agent\":\n",
" continue # just stream from tasks\n",
" print(f\"\\n{task_name}:\")\n",
" if task_name == \"__interrupt__\":\n",
" print(result)\n",
" else:\n",
" result.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': 'Can you reach out for human assistance: what should I feed my cat? Separately, can you check the weather in San Francisco?'}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" human_assistance (call_joAEBVX7Abfm7TsZ0k95ZkVx)\n",
" Call ID: call_joAEBVX7Abfm7TsZ0k95ZkVx\n",
" Args:\n",
" query: What should I feed my cat?\n",
" get_weather (call_ut7zfHFCcms63BOZLrRHszGH)\n",
" Call ID: call_ut7zfHFCcms63BOZLrRHszGH\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content=\"It's sunny!\" name='get_weather' tool_call_id='call_ut7zfHFCcms63BOZLrRHszGH'\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\n",
" \"role\": \"user\",\n",
" \"content\": (\n",
" \"Can you reach out for human assistance: what should I feed my cat? \"\n",
" \"Separately, can you check the weather in San Francisco?\"\n",
" ),\n",
"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that we generate two tool calls, and although our run is interrupted, we did not block the execution of the `get_weather` tool.\n",
"\n",
"Let's inspect where we're interrupted:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'__interrupt__': (Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)}\n"
]
}
],
"source": [
"print(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can resume execution by issuing a [Command](../../concepts/human_in_the_loop/#the-command-primitive). Note that the data we supply in the `Command` can be customized to your needs based on the implementation of `human_assistance`."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content='You should feed your cat a fish.' name='human_assistance' tool_call_id='call_joAEBVX7Abfm7TsZ0k95ZkVx'\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"For human assistance, you should feed your cat fish. \n",
"\n",
"Regarding the weather in San Francisco, it's sunny!\n"
]
}
],
"source": [
"human_response = \"You should feed your cat a fish.\"\n",
"human_command = Command(resume={\"data\": human_response})\n",
"\n",
"for step in agent.stream(human_command, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Above, when we resume we provide the final tool message, allowing the model to generate its response. Check out the LangSmith traces to see a full breakdown of the runs:\n",
"\n",
"1. [Trace from initial query](https://smith.langchain.com/public/c3d8879d-4d01-41be-807e-6d9eed15df99/r)\n",
"2. [Trace after resuming](https://smith.langchain.com/public/97c05ef9-8b4c-428e-8826-3fd417c8c75f/r)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+1 -1
View File
@@ -9,7 +9,7 @@ New to LangGraph or LLM app development? Read this material to get up and runnin
## Get Started 🚀 {#quick-start}
- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [LangGraph Cheatsheet For Common Workflows](workflows.ipynb): Overview of the most common workflows and agent architectures in LangGraph.
- [Common Workflows](workflows/index.md): Overview of the most common workflows using LLMs implemented with LangGraph.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using REST API and LangGraph Studio Web UI.
- [LangGraph Template Quickstart](../concepts/template_applications.md): Start building with LangGraph Platform using a template application.
- [Deploy with LangGraph Cloud Quickstart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -118,6 +118,8 @@ nav:
- how-tos/persistence_postgres.ipynb
- how-tos/persistence_mongodb.ipynb
- how-tos/persistence_redis.ipynb
- how-tos/persistence-functional.ipynb
- how-tos/cross-thread-persistence-functional.ipynb
- Memory:
- Memory: how-tos#memory
- how-tos/memory/manage-conversation-history.ipynb
@@ -132,6 +134,8 @@ nav:
- how-tos/human_in_the_loop/wait-user-input.ipynb
- how-tos/human_in_the_loop/time-travel.ipynb
- how-tos/human_in_the_loop/review-tool-calls.ipynb
- how-tos/wait-user-input-functional.ipynb
- how-tos/review-tool-calls-functional.ipynb
- Streaming:
- Streaming: how-tos#streaming
- how-tos/stream-values.ipynb
@@ -163,6 +167,8 @@ nav:
- how-tos/agent-handoffs.ipynb
- how-tos/multi-agent-network.ipynb
- how-tos/multi-agent-multi-turn-convo.ipynb
- how-tos/multi-agent-network-functional.ipynb
- how-tos/multi-agent-multi-turn-convo-functional.ipynb
- State Management:
- State Management: how-tos#state-management
- how-tos/state-model.ipynb
@@ -185,6 +191,7 @@ nav:
- how-tos/create-react-agent-hitl.ipynb
- how-tos/create-react-agent-structured-output.ipynb
- how-tos/react-agent-from-scratch.ipynb
- how-tos/react-agent-from-scratch-functional.ipynb
- LangGraph Platform:
- LangGraph Platform: how-tos#langgraph-platform
- Application Structure:
@@ -265,6 +272,7 @@ nav:
- concepts/persistence.md
- concepts/memory.md
- concepts/streaming.md
- concepts/functional_api.md
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- High Level:
@@ -296,7 +304,7 @@ nav:
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- tutorials/workflows.ipynb
- tutorials/workflows/index.md
- tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots: