Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36fa8e0097 | ||
|
|
bcb11640a5 | ||
|
|
b6c3a0d0fd | ||
|
|
8b14b6a9f0 | ||
|
|
8c63cc1778 | ||
|
|
6756e91ffc | ||
|
|
fc5d919aee | ||
|
|
9bd351b80f | ||
|
|
83c3f86159 | ||
|
|
3a41a2addc | ||
|
|
3eca363c23 | ||
|
|
2f1e864570 | ||
|
|
79a1ce6804 | ||
|
|
deb99a9acb | ||
|
|
781d0cf27a | ||
|
|
8a02ddd868 | ||
|
|
5f7dcbb07a | ||
|
|
9615580a66 | ||
|
|
d45eb0f9f2 | ||
|
|
4caa483478 | ||
|
|
006305f6a8 | ||
|
|
d9b7aaa5cc | ||
|
|
d2794eda0a | ||
|
|
9cbef9b542 | ||
|
|
343dc2d37a | ||
|
|
ca0ff1d334 | ||
|
|
f5663ffa49 | ||
|
|
e7477a9315 | ||
|
|
18c083b60a | ||
|
|
2f0e3c66d1 | ||
|
|
d9ec185e72 | ||
|
|
87fba0ecd0 | ||
|
|
30e6b5482f | ||
|
|
77b42e0867 | ||
|
|
cbe92e3e55 | ||
|
|
70f2efc9a1 | ||
|
|
dbf5b4920e | ||
|
|
9291ae8646 | ||
|
|
2713082707 | ||
|
|
89eb938b30 | ||
|
|
26e97492b7 | ||
|
|
4c3958f0be | ||
|
|
980b592631 | ||
|
|
e494869c72 | ||
|
|
da0aac7556 | ||
|
|
a200027cda | ||
|
|
8cd57f7457 | ||
|
|
26c1f1ee7a | ||
|
|
33af251a09 | ||
|
|
17dc588c81 | ||
|
|
258060593b | ||
|
|
fbe513835f | ||
|
|
f319b1e107 | ||
|
|
cbb7348998 | ||
|
|
2d8246e7c4 | ||
|
|
e5ea4f51c7 | ||
|
|
a031f8294e | ||
|
|
7d940a4a96 | ||
|
|
6ae0c83c83 | ||
|
|
083a14c2c5 | ||
|
|
e4db5c2ca4 | ||
|
|
1d2b50e438 | ||
|
|
5146c9fcdf | ||
|
|
e8a2f7ef92 | ||
|
|
0400c5236e | ||
|
|
67f96063e2 | ||
|
|
44cdbc781f | ||
|
|
f642fb6545 | ||
|
|
ff3bc2f982 | ||
|
|
e1925a8dcb | ||
|
|
fe83a151bb | ||
|
|
d18c9449ec | ||
|
|
12a15c3cb4 | ||
|
|
189358cb91 | ||
|
|
d16004c0b6 | ||
|
|
fdf19a5be9 | ||
|
|
d24ce62c3f | ||
|
|
3ffed8d38d | ||
|
|
25d4512744 | ||
|
|
630195a108 | ||
|
|
475e16b8ed | ||
|
|
32702dea08 | ||
|
|
3db266bb93 | ||
|
|
11ce54d7e4 | ||
|
|
54a5e45d21 | ||
|
|
0ad470162c | ||
|
|
37436c5cd6 | ||
|
|
7f48428d16 | ||
|
|
d9c0a5d827 | ||
|
|
6907d1b775 | ||
|
|
bec3055561 | ||
|
|
2f535a803c | ||
|
|
fbb11a6d2e | ||
|
|
3f348e3268 | ||
|
|
6b80fa6718 | ||
|
|
82fa597e84 | ||
|
|
f6c44ec154 | ||
|
|
a03900be7a | ||
|
|
7144f7db41 | ||
|
|
8f1db66c17 | ||
|
|
c2556aa2fe | ||
|
|
d468655f62 | ||
|
|
6153c777fb | ||
|
|
a4d49b4e77 | ||
|
|
789c732866 | ||
|
|
51f85ffa84 | ||
|
|
31dd6c65a9 | ||
|
|
cb225dde7f | ||
|
|
0cb530e588 | ||
|
|
f0f3e11b0e | ||
|
|
55fbff89f4 | ||
|
|
327bb369d7 | ||
|
|
30eb2d00e2 | ||
|
|
45d1033092 | ||
|
|
5a0ae2157c | ||
|
|
e001b7a35f | ||
|
|
a41b9bb83c | ||
|
|
04f6a6ccd1 | ||
|
|
ce15790210 | ||
|
|
04b76f55a0 | ||
|
|
f52a8728ff | ||
|
|
686ee31b75 | ||
|
|
47dcb2d105 | ||
|
|
3a611fb20d | ||
|
|
baa88f3c97 | ||
|
|
79aa88812d | ||
|
|
81436ceef8 | ||
|
|
2b70dba0e0 | ||
|
|
e14a6cf98b | ||
|
|
dc0398efd1 | ||
|
|
02f1904ba7 | ||
|
|
30f852e7b2 | ||
|
|
7fc6c4b1fa | ||
|
|
7f8ec2c590 | ||
|
|
611588613d | ||
|
|
11e80210a2 | ||
|
|
60d742ea48 | ||
|
|
a7ac9ffd4e | ||
|
|
3d97b97c86 | ||
|
|
a7d1ecbb74 | ||
|
|
61f362f16e | ||
|
|
7cabc0a3dc | ||
|
|
f9cdfd3ac4 | ||
|
|
dd778f8ed6 | ||
|
|
df5d08f689 | ||
|
|
a9b94f93ee | ||
|
|
5f869b9e75 | ||
|
|
79562f3f37 | ||
|
|
081b2cbdcf | ||
|
|
70eeb2a670 | ||
|
|
0f287d986b | ||
|
|
1fd9da6718 | ||
|
|
ef6c5b4711 | ||
|
|
77fe51fbe4 | ||
|
|
70a5ef6713 | ||
|
|
17c1a8db46 | ||
|
|
97a51014c3 | ||
|
|
5a30fc6a87 | ||
|
|
1f68bd0d83 | ||
|
|
fdfc5d9cda | ||
|
|
1c3f65c931 | ||
|
|
a9f5507006 | ||
|
|
59bfa5d009 | ||
|
|
b4f11929f8 | ||
|
|
038bec2e78 | ||
|
|
01cdb60b5d | ||
|
|
5bfb9af5fe | ||
|
|
ac48612abb | ||
|
|
73fb725f0c | ||
|
|
b98a1337a5 | ||
|
|
c2a41039de | ||
|
|
33fe467d1f | ||
|
|
43b6c06f5c | ||
|
|
d81dec653d | ||
|
|
e1d8c6b113 | ||
|
|
a64f9f80c0 | ||
|
|
a879de51f1 | ||
|
|
60ab76c3e9 | ||
|
|
09e9117674 | ||
|
|
acac19b95b | ||
|
|
723bcfeaa2 | ||
|
|
c279421cbf | ||
|
|
df1e48154a | ||
|
|
d0bf7837bd | ||
|
|
a403e802fa | ||
|
|
e0a0958a60 | ||
|
|
3f1bdb9ebf | ||
|
|
b37c9d8a01 | ||
|
|
1af1911aad | ||
|
|
b4f7e06a1d | ||
|
|
1dda28f8fb | ||
|
|
cc4718c5cb | ||
|
|
0d580bdac7 | ||
|
|
a19d06e18c | ||
|
|
e16312da3f | ||
|
|
6784a5a5b1 | ||
|
|
4e0e9a4eff | ||
|
|
015bf5e0a6 | ||
|
|
85fc26db43 | ||
|
|
5fa80e2a92 | ||
|
|
5e13460604 | ||
|
|
750b97349e | ||
|
|
6230c46830 | ||
|
|
93e4c8cc1f | ||
|
|
b7e441d781 | ||
|
|
ccd8920eef | ||
|
|
0c379d6cc7 | ||
|
|
01b1080b6e | ||
|
|
62ff2eb32d | ||
|
|
1f745ca017 | ||
|
|
aa4fea48dd | ||
|
|
4fd261765a | ||
|
|
0f0e31df24 | ||
|
|
a275ab26d3 | ||
|
|
b3bf4dd43c | ||
|
|
b7fd391811 | ||
|
|
cf961a286c | ||
|
|
4b83103cf2 | ||
|
|
1a46537c3a | ||
|
|
0a49f3003b | ||
|
|
e80098e297 | ||
|
|
3d3647cd85 | ||
|
|
291379dfb9 | ||
|
|
9f93e48a67 | ||
|
|
de123d66a5 | ||
|
|
759a712f57 | ||
|
|
9f73dfa8d5 | ||
|
|
4459952e72 | ||
|
|
8ef82f3578 | ||
|
|
73e3f5a5b0 | ||
|
|
a54587cff5 | ||
|
|
63ea71548b | ||
|
|
f32cf5e984 | ||
|
|
d1aaa9de8c | ||
|
|
2fa2469967 | ||
|
|
de86a46b3d | ||
|
|
9733db03c5 | ||
|
|
e1f65012e6 | ||
|
|
eb593d47dd | ||
|
|
4e8f4ce440 | ||
|
|
007d7e72b1 | ||
|
|
2b77fdabee | ||
|
|
40d16593c7 | ||
|
|
ec7bbe14b2 | ||
|
|
4c6323c585 | ||
|
|
2fe38f3940 | ||
|
|
09ca964714 | ||
|
|
0663d46c47 | ||
|
|
a91dbf9b70 | ||
|
|
d93be914c7 | ||
|
|
287c29fbdc | ||
|
|
90dd2b01b6 | ||
|
|
a443b3b256 | ||
|
|
2e9aea6fc8 | ||
|
|
2895a69678 | ||
|
|
76a209835f | ||
|
|
872f54adf1 | ||
|
|
01a3c23a29 | ||
|
|
0461d45d76 | ||
|
|
7d8205633d | ||
|
|
c75bfc1032 | ||
|
|
6dc70b703d | ||
|
|
eb09909c22 | ||
|
|
f08155d60b | ||
|
|
24b16908b7 | ||
|
|
c1c2ce8f1b | ||
|
|
3efd4f3406 | ||
|
|
f122ae2eb1 | ||
|
|
05791f5dfc | ||
|
|
416dfe95da | ||
|
|
2d6ddd0a1d | ||
|
|
253090f34d | ||
|
|
7d80176137 | ||
|
|
ca7da2fc41 |
@@ -42,7 +42,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: lint-${{ inputs.working-directory }}
|
||||
|
||||
- name: Check Poetry File
|
||||
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: test-${{ inputs.working-directory }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -29,7 +29,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: release
|
||||
|
||||
# We want to keep this build stage *separate* from the release stage,
|
||||
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: release
|
||||
|
||||
# We want to keep this build stage *separate* from the release stage,
|
||||
@@ -169,7 +168,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Import published package
|
||||
shell: bash
|
||||
@@ -256,7 +254,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: release
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
@@ -298,7 +295,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: release
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
@@ -13,7 +13,7 @@ serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint --dirty
|
||||
poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint -w ./libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs/docs -name "*.ipynb" -type f -delete
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
eNqNVmtsFFUULqJREoMJz0R+MKxAUXd2Z7ZL220atfQFpS3QWdJCUza3M3d3hs7eO8y903ZtmkjFQJRIJkRjYsBAt7u6KX2EGjTlYYQYmsgf/khVIMbE+EMTjSQkRsA7s7t9F+if7t5zzne+c75zTrYv3QlNomG0ZFBDFJpApuwLsfvSJjxkQUKPpOKQqlhJ7t4lhfstU5vcpFJqkDK/HxiaDyCqmtjQZJ+M4/5O0R+HhIAYJMl2rCR+fOZijycOuiMUd0BEPGWcKASCXs6T92IvrT0eE+uQffJYBJoeZpUxo4Ko89SlAlpIOKpCrgsC9s/kNMSR6JueXi83HQkI0QhlZGaHM2wKu12gHYW6zqlQN7gEtjhZhXLHXFQJIK7GBEjWiIx9DhJNGC68C+ImnHrBWI8wvo4XAnH3LQcVIRCYsupYNGRYTvYej6zRhOMzK4eX2ytVOLgeTcmDWhFBrEkYAYV2ldS2N9cXt6jbVKlREls8vW2zap7frdY5/ExILH1OSzyShVBig1tdrobIU2V3WJIINE1sMu8o0AlkhNocMbECdQdB1oGlQL6I38oTjBCkvA4om6F8spzci/dLgUQ2NcMZQZeqa+Ci2Jwp1VRjI4TJGAdufw02hNCkmjtSU+2e7gehpoZinl6n2c5kayZ0im7NerbN0Bq3H4QyU5sVl1YhUNh6nEiqmFB7dN7ADwNZhgblIZKxwvDtc7G3NcPLKTDqVJ6RnS64G2VnOiA0eKBrnTCVjbJHgGHomgwcu/8g69hgTibe4TLfnHEU59naIGpfqMjz8O9OsP1EnOArCvoCI9082wMN6WzBWPcZpZTh2sdnGgwgdzAcPrf7diobPDTTBxN7oAHIu6RZkI4g9gAw48XB8zPfTQtRLQ7tdOXu+elyxul0RT5R9IVGZwGTBJLtAXeuRqeaPBWSCQiBIl4o5gVxKN8lHaIYVe3+EkH4nI26we4WfDfFIKlF+pJMEfj99XTu0pzdtTOv5p2Ctckqpo59qRkqXk4UuSoocww/yImlZYJQJga52obwYGUuTXhBMUbDbItJlAlSnRc/LasW6oBKpnJB2Sc902WZLL+uxTXK584sE8v5aieDgiBMbn6sp8mGXkNOxmRRKBR6Ai7rDKT2mFMfLwZ4UQznqizav3Aed7f47MXOsUo5rBiv157oP80tH7P5KWIWYRjcP1m4UDS26DyKA6Vuttef7D9NMRdT+DQxi1PkFgqf075soo2P8ZzZuKw391jvRflkcsrzmmJfZJ/ZRa9orK1Vw1rXwdD+urpY4yEFVVcF6vs7NWBnRJ/IxTCO6XC4soavBOyk8pK7Qna6al9jRcOOysEWvgm3YzZLYcBmDmEEUxI02WraGVnHlsKOnQlTLLypYp89VhoVSrZGS9rF0kBIiBYH+ermppH8Mk0tS9K5lO6PhMOp7HG+9mD9By8UuH9L60/cqLv61qr3IrB8XcnK7e2rT8GKtUOrzqyzhQbfxK2JU2X3P/zy48vjY1u+Kvvr1PrS548sWX7+eNvt8uHiw90H7pZ0PPz59tDdzy43//OKl14ZkN6v+m/7yPVXT7+zdtnZ06vXkJe3fHvmxOUXw217f/hoY/Rm5txP1dIF3vvvtbpBY/jolT0Dw9ID7/Kd/uAn12oKA9veuH9yYuOmTMvKe03HV/12aZl+oPzTo398/dLNwpNXS7/4ZWz8m79vbAvGx+sPrLh6uPv3MeW74Q339hzr6ft1RX916K75/LN31vwZjB97Dh2SWpNnH+0jXeUPWZ2PHi0t2Hyrd2LrkoKC/wFNFp9z
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
eNqNVX1sE2UY3xgkOiAQVFSUcDYogrvu2l63dcboKGwO2IdrHYxl1Ld3b9tj17vj3ve6DkLCV4JR+Tid8vGHICutlAqbbqAyTAbRQSCIxIhDlIQ/FELUEUKIRjPfu7X7Fu0/vXvf5+P3/H7P89ymRASqSJCl7JQgYagCDpMXpG9KqHCNBhHeEg9DHJL5WHWVx9uqqULv0yGMFVScnw8UwQokHFJlReCsnBzOj9jywxAhEIQo5pf55is5U9ZZwiDqw3IjlJClmLIxdjaPsmSsyEn9Oosqi5A8WTQEVQu55WQCRcLGUVMI4HmIwiFINUFA/lRKkCgUeNGyPo8a8gQICQgTMCPdSWwMo2Ygr0wFIR4VKCCrYWAUTJEnygMkqlQFEicgTs6jyimOHBBMw518CAKVC1EBTTKZslLLSNQwpAIQk2NM4I6J2yxrVgMXblZMsCYkE/7giSyLPpLJsJJA2DwbmdC4ESRFM2pZZ+EE3GzYjEL8qqfEiGsR+ExQzcfY6rx8nX+h2x6pq8OFfjfkAjjkbLKsbxjB4Fju60fhUyHSxFEEWzyaJDU/ZVaXrsH3v7IbKJEPqqqsEusAEBEkgBqM1pB5KBoROBFoPKQdtJNGsiRBTNtJ8zAFdiaTLt0+/84YDxGnCoqhhAk2rR3RZJikg9T6EBeCYWAyrJCmhioWzBYdJHyIEYRVQQpa1ht0G5MiqNAou37AsmGY2rJ/NeSI3qS8RAgCnozbjlhIRlhvHzNARwHHQQXTUOJknsTXPwquFZQ8iocBEWCY5AwezL7Tk40QKjQQhQiMD3jpbUBRRIEzGy9/NeEslRaKNrCMvU4amtNkDCWsHy/J4MivbibzLlGM1cFa7W1RmsyVIIlkYGkREEhxxbw/MfxCAVwjiUOnd4keH3A+MtxGRvrBCsBVeUaENATRDwI1XMB+Mvxc1SQshKGecFePTZe+HErnsNpsVlf7iMCoWeL0g2ZntQ+SPOiSJL3koJkCmrEdybAkQimIQ3prYRHzIWl2hexBuDlOQmINbYoRReD5M4n05jpQtTSj5k9ZM2KLiDr6SW9Iy6PsBZQHKpTRq5TNUewoKLY7qLIKb8qdTuMdV4x2L5ljFCCCLM6In+BCmtQI+aR7XNl7LUNlqSS/KIQFTKfXNhHLeNVjLMMwvc/c11IlTS9IRsaYw+Vy/UdcwgzEeodRH824aHuBd6BKJ7uylxrPc2D3p/HEDTwE0dz7WA7hyVhT97UeH4/dsTKZBk0LvN5Fnsk6WuYvjIZKK/2SVBtdGaldoYJFTtvazijNibLG05h8ACFtNkQU670Uy7N8EWPjgJ9leSfJUcAGGMj7eQY4bX4H1xoRgJ60WW1UUJaDIjzqLqXdgKwR2mO2jZ5YVFdZUlHuTq2ga2S/TPjzAsKzJEsw7oEqaUc9aaYmA67COHGvKanTO4o4lwOwhUU8LHJwLOukFy+vacs00GCDxIztYH5oN8YHFtKX2ZVz3nwgy/zleKu7l+S8MuWvd3cvza8/1El/+mhJd01f7cXNL5dfi/9Y5nvnz7eDDUv6G449e2P+aT7w2M9b2rtn3TvWsE1atWpX07mqfZ7vnaee3P71nZ6K7tyTzytH/Y4r9trdzrsT9yxGM3py711RTs1yH7jU1xcpmr33jbZpPa23efaJHW2n9l/P2373h5uF3/5x+Livcts0/uqG3Oi515a5Onb2LlEPW6/qZ+svTJ/03ty+lvKFHfunT029jpe39W3M7t57KHq1ZeJU7eQXx7femJ06MXmVp2nq5FtrAtTNrm0fT6r86qLwOLvz+vbuRyIHyvZ3zuy/XIJ+n/D3/I5vuubtWNC24aUJdMI9Z2LPjV9feP/ac+zl0wsce3K/O3Mp1cV379t6uww8+FZnS8uCvF2dtx4qdT3s++y3FVNuncW1H8zsP1fR88udQkJcf39O1ucX2Mbz2VlZ/wBwZ4U6
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 642 KiB |
@@ -36,8 +36,8 @@ Dependencies can optionally be specified in one of the following files: `pyproje
|
||||
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
|
||||
|
||||
```
|
||||
langgraph>=0.2.30,<0.3.0
|
||||
langgraph-checkpoint>=1.0.14
|
||||
langgraph>=0.2.56,<0.3.0
|
||||
langgraph-checkpoint>=2.0.5,<3.0
|
||||
langchain-core>=0.2.38,<0.4.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.9.7
|
||||
|
||||
@@ -36,8 +36,8 @@ Dependencies can optionally be specified in one of the following files: `pyproje
|
||||
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
|
||||
|
||||
```
|
||||
langgraph>=0.2.30,<0.3.0
|
||||
langgraph-checkpoint>=1.0.14
|
||||
langgraph>=0.2.56,<0.3.0
|
||||
langgraph-checkpoint>=2.0.5,<3.0
|
||||
langchain-core>=0.2.38,<0.4.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.9.7
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Adding nodes as dataset examples in Studio
|
||||
|
||||
In LangGraph Studio you can create dataset examples from the thread history in the right-hand pane. This can be especially useful when you want to evaluate intermediate steps of the agent.
|
||||
|
||||
1. Click on the `Add to Dataset` button to enter the dataset mode.
|
||||
1. Select nodes which you want to add to dataset.
|
||||
1. Select the target dataset to create the example in.
|
||||
|
||||
You can edit the example payload before sending it to the dataset, which is useful if you need to make changes to conform the example to the dataset schema.
|
||||
|
||||
Finally, you can customise the target dataset by clicking on the `Settings` button.
|
||||
|
||||
See [Evaluating intermediate steps](https://docs.smith.langchain.com/evaluation/how_to_guides/langgraph#evaluating-intermediate-steps) for more details on how to evaluate intermediate steps.
|
||||
|
||||
<video controls allowfullscreen="true" poster="../img/studio_datasets.jpg">
|
||||
<source src="https://langgraph-docs-assets.pages.dev/studio_datasets.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
After Width: | Height: | Size: 170 KiB |
@@ -1,462 +1,273 @@
|
||||
# LangGraph Cloud Quick Start
|
||||
# Quickstart: Deploy on LangGraph Cloud
|
||||
|
||||
In this tutorial you will build and deploy a simple chatbot agent that can look things up on the internet. You will be using [LangGraph Cloud](../concepts/langgraph_cloud.md), [LangGraph Studio](../concepts/langgraph_studio.md) to visualize and test it out, and [LangGraph SDK](./reference/sdk/python_sdk_ref.md) to interact with the deployed agent.
|
||||
!!! note "Prerequisites"
|
||||
|
||||
If you want to learn how to build an agent like this from scratch, take a look at the [LangGraph Quick Start tutorial](../tutorials/introduction.ipynb).
|
||||
Before you begin, ensure you have the following:
|
||||
|
||||
## Set up requirements
|
||||
- [GitHub account](https://github.com/)
|
||||
- [LangSmith account](https://smith.langchain.com/)
|
||||
|
||||
This tutorial will use:
|
||||
## Create a repository on GitHub
|
||||
|
||||
- Anthropic for the LLM - sign up and get an API key [here](https://console.anthropic.com/).
|
||||
- Tavily for the search engine - sign up and get an API key [here](https://app.tavily.com/).
|
||||
- LangSmith for hosting - sign up and get an API key [here](https://smith.langchain.com/).
|
||||
To deploy a LangGraph application to **LangGraph Cloud**, your application code must reside in a GitHub repository. Both public and private repositories are supported.
|
||||
|
||||
## Create and configure your app
|
||||
You can deploy any [LangGraph Application](../concepts/application_structure.md) to LangGraph Cloud.
|
||||
|
||||
First, let's set create all of the necessary files for our LangGraph application.
|
||||
For this guide, we'll use the pre-built Python [**ReAct Agent**](https://github.com/langchain-ai/react-agent) template.
|
||||
|
||||
1. __Create application directory and files__
|
||||
??? note "Get Required API Keys for the ReAct Agent template"
|
||||
|
||||
Create a new application `my-app` with the following file structure:
|
||||
This **ReAct Agent** application requires an API key from [Anthropic](https://console.anthropic.com/) and [Tavily](https://app.tavily.com/). You can get these API keys by signing up on their respective websites.
|
||||
|
||||
```shell
|
||||
mkdir my-app
|
||||
```
|
||||
**Alternative**: If you'd prefer a scaffold application that doesn't require API keys, use the [**New LangGraph Project**](https://github.com/langchain-ai/new-langgraph-project) template instead of the **ReAct Agent** template.
|
||||
|
||||
=== "Python"
|
||||
|
||||
my-app/
|
||||
|-- agent.py # code for your LangGraph agent
|
||||
|-- requirements.txt # Python packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
my-app/
|
||||
|-- agent.ts # code for your LangGraph agent
|
||||
|-- package.json # Javascript packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
|
||||
1. __Define your graph__
|
||||
|
||||
=== "Python"
|
||||
The `agent.py` file should contain code with your graph.
|
||||
|
||||
=== "Javascript"
|
||||
The `agent.ts` file should contain code with your graph.
|
||||
|
||||
The following code example is a simple chatbot agent (similar to the one in the [previous tutorial](../tutorials/introduction.ipynb)). Specifically, it uses [create_react_agent][langgraph.prebuilt.chat_agent_executor.create_react_agent], a prebuilt [ReAct](../concepts/agentic_concepts.md#react-implementation)-style agent.
|
||||
|
||||
The `agent` file needs to have a variable with a [CompiledGraph][langgraph.graph.graph.CompiledGraph] (in this case the `graph` variable).
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# agent.py
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
|
||||
|
||||
tools = [TavilySearchResults(max_results=2)]
|
||||
|
||||
# compiled graph
|
||||
graph = create_react_agent(model, tools)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```ts
|
||||
// agent.ts
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const model = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
});
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
|
||||
// compiled graph
|
||||
export const graph = createReactAgent({ llm: model, tools });
|
||||
```
|
||||
|
||||
1. __Specify dependencies__
|
||||
|
||||
=== "Python"
|
||||
You should add dependencies for your graph(s) to `requirements.txt`.
|
||||
|
||||
=== "Javascript"
|
||||
You should add dependencies for your graph(s) to `package.json`.
|
||||
|
||||
In this case we only require four packages for our graph to run:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "my-app",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.3.11",
|
||||
"@langchain/core": "^0.3.16",
|
||||
"@langchain/langgraph": "0.2.18",
|
||||
"@langchain/anthropic": "^0.3.7"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. __Create LangGraph configuration file__
|
||||
|
||||
The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to deploy. In this case we only have one graph: the compiled `graph` object from `agent.py` / `agent.ts`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
|
||||
|
||||
1. __Specify environment variables__
|
||||
|
||||
The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step.
|
||||
|
||||
!!! warning
|
||||
The `.env` file should NOT be included with the rest of source code in your Github repository. When creating a deployment using LangGraph Cloud, you will be able to specify the environment variables manually.
|
||||
|
||||
For this graph, we need two environment variables:
|
||||
|
||||
```shell
|
||||
ANTHROPIC_API_KEY=...
|
||||
TAVILY_API_KEY=...
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Learn more about different application structure options [here](../how-tos/index.md#application-structure).
|
||||
|
||||
Now that we have set everything up on our local file system, we are ready to test our graph locally.
|
||||
|
||||
## Test the app locally
|
||||
|
||||
To test the LangGraph app before deploying it using LangGraph Cloud, you can start the [LangGraph server](../concepts/langgraph_server.md) locally or use [LangGraph Studio](../concepts/langgraph_studio.md).
|
||||
|
||||
## Using local server
|
||||
|
||||
You can test your app by running [LangGraph server](../concepts/langgraph_server.md) locally. This is useful to make sure you have configured our [CLI configuration file][langgraph.json] correctly and can interact with your graph.
|
||||
|
||||
To run the server locally, you need to first install the LangGraph CLI:
|
||||
|
||||
```shell
|
||||
pip install langgraph-cli
|
||||
```
|
||||
|
||||
You can then test our API server locally. In order to run the server locally, you will need to add your `LANGSMITH_API_KEY` to the `.env` file.
|
||||
|
||||
```shell
|
||||
langgraph up
|
||||
```
|
||||
|
||||
This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
|
||||
|
||||
```shell
|
||||
Ready!
|
||||
- API: http://localhost:8123
|
||||
```
|
||||
|
||||
First, let's verify that the server is running correctly by calling `/ok` endpoint:
|
||||
|
||||
```shell
|
||||
curl --request GET --url http://localhost:8123/ok
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
{"ok": "true"}
|
||||
```
|
||||
|
||||
Now we're ready to test the app with the real inputs!
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://localhost:8123/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": "agent",
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the weather in NYC?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stream_mode": "updates"
|
||||
}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
...
|
||||
|
||||
data: {
|
||||
"agent": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "The search results from Tavily provide the current weather conditions in New York City, including temperature, wind speed, precipitation, humidity, and cloud cover. According to the results, as of 3:00pm on October 30th, 2024, it is overcast in NYC with a temperature of around 66°F (19°C), light winds from the southwest around 8 mph (13 km/h), and 66% humidity.\n\nSo in summary, the current weather in NYC is overcast with mild temperatures in the mid 60sF and light winds, based on the search results. Let me know if you need any other details!",
|
||||
"type": "ai",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can see that our agent responds with the up-to-date search results!
|
||||
|
||||
### Using LangGraph Studio Desktop
|
||||
|
||||
You can also test your app locally with [LangGraph Studio](../concepts/langgraph_studio.md). LangGraph Studio offers a new way to develop LLM applications by providing a specialized agent IDE that enables visualization, interaction, and debugging of complex agentic applications.
|
||||
|
||||
With visual graphs and the ability to edit state, you can better understand agent workflows and iterate faster. LangGraph Studio integrates with LangSmith allowing you to collaborate with teammates to debug failure modes.
|
||||
|
||||
LangGraph Studio is available as a [desktop app](https://studio.langchain.com/) for MacOS users. Once you have installed the app, you can select `my-app` directory, which will automatically start the server locally and load the graph in the UI.
|
||||
|
||||
To interact with your chatbot agent in LangGraph Studio, you can add a new message in the `Input` section and press `Submit`.
|
||||
|
||||

|
||||
1. Go to the [ReAct Agent](https://github.com/langchain-ai/react-agent) repository.
|
||||
2. Fork the repository to your GitHub account by clicking the `Fork` button in the top right corner.
|
||||
|
||||
## Deploy to LangGraph Cloud
|
||||
|
||||
Once you've tested your graph locally and verified that it works as expected, you can deploy it to the LangGraph Cloud.
|
||||
??? note "1. Log in to [LangSmith](https://smith.langchain.com/)"
|
||||
|
||||
First, you'll need to turn the `my-app` directory into a GitHub repo and [push it to GitHub](https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github).
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/01_login.png)
|
||||
<figcaption>
|
||||
Go to [LangSmith](https://smith.langchain.com/) and log in. If you don't have an account, you can sign up for free.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Once you have created your GitHub repository with a Python file containing your compiled graph as well as a `langgraph.json` with the configuration, you can head over to [LangSmith](https://smith.langchain.com/) and click on the graph icon (`LangGraph Cloud`) on the bottom of the left navbar. This will open the LangGraph deployments page. On this page, click the `+ New Deployment` button in the top right corner.
|
||||
|
||||

|
||||
??? note "2. Click on <em>LangGraph Platform</em> (the left sidebar)"
|
||||
|
||||
**_If you have not deployed to LangGraph Cloud before:_** there will be a button that shows up saying `Import from GitHub`. You’ll need to follow that flow to connect LangGraph Cloud to GitHub.
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/02_langgraph_platform.png)
|
||||
<figcaption>
|
||||
Select **LangGraph Platform** from the left sidebar.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
**_Once you have set up your GitHub connection:_** the new deployment page will look as follows:
|
||||
??? note "3. Click on + New Deployment (top right corner)"
|
||||
|
||||

|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/03_deployments_page.png)
|
||||
<figcaption>
|
||||
Click on **+ New Deployment** to create a new deployment. This button is located in the top right corner.
|
||||
It'll open a new modal where you can fill out the required fields.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
To deploy your application, you should do the following:
|
||||
??? note "4. Click on Import from GitHub (first time users)"
|
||||
|
||||
1. Select your GitHub username or organization from the selector
|
||||
1. Search for your repo to deploy in the search bar and select it
|
||||
1. Choose a name for your deployment
|
||||
1. In the `Git Branch` field, you can specify either the branch for the code you want to deploy, or the exact commit SHA.
|
||||
1. In the `LangGraph API config file` field, enter the path to your `langgraph.json` file (which in this case is just `langgraph.json`)
|
||||
1. If your application needs environment variables, add those in the `Environment Variables` section. They will be propagated to the underlying server so your code can access them. In this case, we will need `ANTHROPIC_API_KEY` and `TAVILY_API_KEY`.
|
||||
<figure markdown="1">
|
||||
[](deployment/img/04_create_new_deployment.png)
|
||||
<figcaption>
|
||||
Click on **Import from GitHub** and follow the instructions to connect your GitHub account. This step is needed for **first-time users** or to add private repositories that haven't been connected before.</figcaption>
|
||||
</figure>
|
||||
|
||||
Hit `Submit` and your application will start deploying!
|
||||
??? note "5. Select the repository, configure ENV vars etc"
|
||||
|
||||
After your deployment is complete, your deployments page should look as follows:
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/05_configure_deployment.png)
|
||||
<figcaption>
|
||||
Select the <strong>repository</strong>, add env variables and secrets, and set other configuration options.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||

|
||||
- **Repository**: Select the repository you forked earlier (or any other repository you want to deploy).
|
||||
- Set the secrets and environment variables required by your application. For the **ReAct Agent** template, you need to set the following secrets:
|
||||
- **ANTHROPIC_API_KEY**: Get an API key from [Anthropic](https://console.anthropic.com/).
|
||||
- **TAVILY_API_KEY**: Get an API key on the [Tavily website](https://app.tavily.com/).
|
||||
|
||||
## Interact with your deployment
|
||||
??? note "6. Click Submit to Deploy!"
|
||||
|
||||
### Using LangGraph Studio (Cloud)
|
||||
|
||||
On the deployment page for your application,, you should see a button in the top right corner that says `LangGraph Studio`. Clicking on this button will take you to the web version of LangGraph Studio. This is the same UI that you interacted with when [testing the app locally](#using-langgraph-studio-recommended), but instead of using a local LangGraph server, it uses the one from your LangGraph Cloud deployment.
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/05_configure_deployment.png)
|
||||
<figcaption>
|
||||
Please note that this step may ~15 minutes to complete. You can check the status of your deployment in the **Deployments** view.
|
||||
Click the <strong>Submit</strong> button at the top right corner to deploy your application.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||

|
||||
|
||||
### Using LangGraph SDK
|
||||
## Lagraph Studio Web UI
|
||||
|
||||
You can also interact with your deployed LangGraph application programmatically, using [LangGraph SDK](./reference/sdk/python_sdk_ref.md).
|
||||
Once your application is deployed, you can test it in **LangGraph Studio**.
|
||||
|
||||
First, make sure you have the SDK installed:
|
||||
??? note "1. Click on an existing deployment"
|
||||
|
||||
=== "Python"
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/07_deployments_page.png)
|
||||
<figcaption>
|
||||
Click on the deployment you just created to view more details.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```shell
|
||||
pip install langgraph_sdk
|
||||
```
|
||||
??? note "2. Click on LangGraph Studio"
|
||||
|
||||
=== "Javascript"
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:300px"}](deployment/img/08_deployment_view.png)
|
||||
<figcaption>
|
||||
Click on the <strong>LangGraph Studio</strong> button to open LangGraph Studio.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```shell
|
||||
yarn add @langchain/langgraph-sdk
|
||||
```
|
||||
<figure markdown="1">
|
||||
[{: style="max-height:400px"}](deployment/img/09_langgraph_studio.png)
|
||||
<figcaption>
|
||||
Sample graph run in LangGraph Studio.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Before using, you need to get the URL of your LangGraph deployment. You can find this in the `Deployment` view. Click the URL to copy it to the clipboard.
|
||||
## Test the API
|
||||
|
||||
You also need to make sure you have set up your API key properly so you can authenticate with LangGraph Cloud.
|
||||
!!! note
|
||||
|
||||
The API calls below are for the **ReAct Agent** template. If you're deploying a different application, you may need to adjust the API calls accordingly.
|
||||
|
||||
Before using, you need to get the `URL` of your LangGraph deployment. You can find this in the `Deployment` view. Click the `URL` to copy it to the clipboard.
|
||||
|
||||
You also need to make sure you have set up your API key properly, so you can authenticate with LangGraph Cloud.
|
||||
|
||||
```shell
|
||||
export LANGSMITH_API_KEY=...
|
||||
```
|
||||
|
||||
The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on:
|
||||
=== "Python SDK (Async)"
|
||||
|
||||
=== "Python"
|
||||
**Install the LangGraph Python SDK**
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
```shell
|
||||
pip install langgraph-sdk
|
||||
```
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# get default assistant
|
||||
assistants = await client.assistants.search(metadata={"created_by": "system"})
|
||||
assistant = assistants[0]
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// get default assistant
|
||||
const assistants = await client.assistants.search({ metadata: {"created_by": "system"} })
|
||||
const assistant = assistants[0];
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0,
|
||||
"metadata": {"created_by": "system"}
|
||||
}' &&
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
We can then execute a run on the thread:
|
||||
|
||||
=== "Python"
|
||||
**Send a message to the assistant (threadless run)**
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": [{"role": "user", "content": "What is the weather in NYC?"}]
|
||||
}
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="your-deployment-url", api_key="your-langsmith-api-key")
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"],
|
||||
input=input,
|
||||
None, # Threadless run
|
||||
"agent", # Name of assistant. Defined in langgraph.json.
|
||||
input={
|
||||
"messages": [{
|
||||
"role": "human",
|
||||
"content": "What is LangGraph?",
|
||||
}],
|
||||
},
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data:
|
||||
print(chunk.data)
|
||||
print(f"Receiving new event of type: {chunk.event}...")
|
||||
print(chunk.data)
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
=== "Python SDK (Sync)"
|
||||
|
||||
**Install the LangGraph Python SDK**
|
||||
|
||||
```shell
|
||||
pip install langgraph-sdk
|
||||
```
|
||||
|
||||
**Send a message to the assistant (threadless run)**
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_sync_client
|
||||
|
||||
client = get_sync_client(url="your-deployment-url", api_key="your-langsmith-api-key")
|
||||
|
||||
for chunk in client.runs.stream(
|
||||
None, # Threadless run
|
||||
"agent", # Name of assistant. Defined in langgraph.json.
|
||||
input={
|
||||
"messages": [{
|
||||
"role": "human",
|
||||
"content": "What is LangGraph?",
|
||||
}],
|
||||
},
|
||||
stream_mode="updates",
|
||||
):
|
||||
print(f"Receiving new event of type: {chunk.event}...")
|
||||
print(chunk.data)
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Javascript SDK"
|
||||
|
||||
**Install the LangGraph JS SDK**
|
||||
|
||||
```shell
|
||||
npm install @langchain/langgraph-sdk
|
||||
```
|
||||
|
||||
**Send a message to the assistant (threadless run)**
|
||||
|
||||
```js
|
||||
const input = { "messages": [{ "role": "user", "content": "What is the weather in NYC?" }] };
|
||||
const { Client } = await import("@langchain/langgraph-sdk");
|
||||
|
||||
// only set the apiUrl if you changed the default port when calling langgraph up
|
||||
const client = new Client({ apiUrl: "your-deployment-url", apiKey: "your-langsmith-api-key" });
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
null, // Threadless run
|
||||
"agent", // Assistant ID
|
||||
{
|
||||
input: {
|
||||
"messages": [
|
||||
{ "role": "user", "content": "What is LangGraph?"}
|
||||
]
|
||||
},
|
||||
streamMode: "messages",
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data) {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(JSON.stringify(chunk.data));
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
=== "Rest API"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_ID>,
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the weather in NYC?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stream_mode": "updates"
|
||||
}'
|
||||
curl -s --request POST \
|
||||
--url <DEPLOYMENT_URL> \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {
|
||||
\"messages\": [
|
||||
{
|
||||
\"role\": \"human\",
|
||||
\"content\": \"What is LangGraph?\"
|
||||
}
|
||||
]
|
||||
},
|
||||
\"stream_mode\": \"updates\"
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
...
|
||||
|
||||
data: {
|
||||
"agent": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "The search results from Tavily provide the current weather conditions in New York City, including temperature, wind speed, precipitation, humidity, and cloud cover. According to the results, as of 3:00pm on October 30th, 2024, it is overcast in NYC with a temperature of around 66°F (19°C), light winds from the southwest around 8 mph (13 km/h), and 66% humidity.\n\nSo in summary, the current weather in NYC is overcast with mild temperatures in the mid 60sF and light winds, based on the search results. Let me know if you need any other details!",
|
||||
"type": "ai",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
## Next Steps
|
||||
|
||||
Congratulations! If you've worked your way through this tutorial you are well on your way to becoming a LangGraph Cloud expert. Here are some other resources to check out to help you out on the path to expertise:
|
||||
|
||||
* [LangGraph How-to guides](../how-tos/index.md)
|
||||
* [LangGraph Tutorials](../tutorials/index.md)
|
||||
### LangGraph Framework
|
||||
|
||||
- **[LangGraph Tutorial](../tutorials/introduction.ipynb)**: Get started with LangGraph framework.
|
||||
- **[LangGraph Concepts](../concepts/index.md)**: Learn the foundational concepts of LangGraph.
|
||||
- **[LangGraph How-to Guides](../how-tos/index.md)**: Guides for common tasks with LangGraph.
|
||||
|
||||
### 📚 Learn More about LangGraph Platform
|
||||
|
||||
Expand your knowledge with these resources:
|
||||
|
||||
- **[LangGraph Platform Concepts](../concepts/index.md#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform.
|
||||
- **[LangGraph Platform How-to Guides](../how-tos/index.md#langgraph-platform)**: Discover step-by-step guides to build and deploy applications.
|
||||
- **[Launch Local LangGraph Server](../tutorials/langgraph-platform/local-server.md)**: This quick start guide shows how to start a LangGraph Server locally for the **ReAct Agent** template. The steps are similar for other templates.
|
||||
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ langgraph [OPTIONS] COMMAND [ARGS]
|
||||
|
||||
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
|
||||
|
||||
!!! note "Python only"
|
||||
|
||||
Currently, the CLI only supports Python >= 3.11.
|
||||
JS support is coming soon.
|
||||
|
||||
**Installation**
|
||||
|
||||
This command requires the "inmem" extra to be installed:
|
||||
@@ -253,3 +258,4 @@ RUN set -ex && \
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
|
||||
```
|
||||
@@ -6,3 +6,25 @@
|
||||
|
||||
::: langgraph_sdk.schema
|
||||
handler: python
|
||||
|
||||
|
||||
::: langgraph_sdk.auth
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.types.Authenticator
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.types.Handler
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.types.HandlerResult
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.types.FilterType
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.types.AuthContext
|
||||
handler: python
|
||||
|
||||
::: langgraph_sdk.auth.exceptions
|
||||
handler: python
|
||||
@@ -0,0 +1,132 @@
|
||||
# Breakpoints
|
||||
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](./human_in_the_loop.md#interrupt) for this purpose.
|
||||
|
||||
## Requirements
|
||||
|
||||
To use breakpoints, you will need to:
|
||||
|
||||
1. [**Specify a checkpointer**](persistence.md#checkpoints) to save the graph state after each step.
|
||||
2. [**Set breakpoints**](#setting-breakpoints) to specify where execution should pause.
|
||||
3. **Run the graph** with a [**thread ID**](./persistence.md#threads) to pause execution at the breakpoint.
|
||||
4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](./human_in_the_loop.md#the-command-primitive)).
|
||||
|
||||
## Setting breakpoints
|
||||
|
||||
There are two places where you can set breakpoints:
|
||||
|
||||
1. **Before** or **after** a node executes by setting breakpoints at **compile time** or **run time**. We call these [**static breakpoints**](#static-breakpoints).
|
||||
2. **Inside** a node using the [`NodeInterrupt` exception](#nodeinterrupt-exception).
|
||||
|
||||
### Static breakpoints
|
||||
|
||||
Static breakpoints are triggered either **before** or **after** a node executes. You can set static breakpoints by specifying `interrupt_before` and `interrupt_after` at **"compile" time** or **run time**.
|
||||
|
||||
=== "Compile time"
|
||||
|
||||
```python
|
||||
graph = graph_builder.compile(
|
||||
interrupt_before=["node_a"],
|
||||
interrupt_after=["node_b", "node_c"],
|
||||
checkpointer=..., # Specify a checkpointer
|
||||
)
|
||||
|
||||
thread_config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread"
|
||||
}
|
||||
}
|
||||
|
||||
# Run the graph until the breakpoint
|
||||
graph.invoke(inputs, config=thread_config)
|
||||
|
||||
# Optionally update the graph state based on user input
|
||||
graph.update_state(update, config=thread_config)
|
||||
|
||||
# Resume the graph
|
||||
graph.invoke(None, config=thread_config)
|
||||
```
|
||||
|
||||
=== "Run time"
|
||||
|
||||
```python
|
||||
graph.invoke(
|
||||
inputs,
|
||||
config={"configurable": {"thread_id": "some_thread"}},
|
||||
interrupt_before=["node_a"],
|
||||
interrupt_after=["node_b", "node_c"]
|
||||
)
|
||||
|
||||
thread_config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread"
|
||||
}
|
||||
}
|
||||
|
||||
# Run the graph until the breakpoint
|
||||
graph.invoke(inputs, config=thread_config)
|
||||
|
||||
# Optionally update the graph state based on user input
|
||||
graph.update_state(update, config=thread_config)
|
||||
|
||||
# Resume the graph
|
||||
graph.invoke(None, config=thread_config)
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
You cannot set static breakpoints at runtime for **sub-graphs**.
|
||||
If you have a sub-graph, you must set the breakpoints at compilation time.
|
||||
|
||||
Static breakpoints can be especially useful for debugging if you want to step through the graph execution one
|
||||
node at a time or if you want to pause the graph execution at specific nodes.
|
||||
|
||||
### `NodeInterrupt` exception
|
||||
|
||||
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
|
||||
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
|
||||
|
||||
??? node "`NodeInterrupt` exception"
|
||||
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
|
||||
return state
|
||||
```
|
||||
|
||||
|
||||
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
|
||||
|
||||
```python
|
||||
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
|
||||
|
||||
```python
|
||||
# Update the state to pass the dynamic breakpoint
|
||||
graph.update_state(config=thread_config, values={"input": "foo"})
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
|
||||
|
||||
```python
|
||||
# This update will skip the node `my_node` altogether
|
||||
graph.update_state(config=thread_config, values=None, as_node="my_node")
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
## Additional Resources 📚
|
||||
|
||||
- [**Conceptual Guide: Persistence**](persistence.md): Read the persistence guide for more context about persistence.
|
||||
- [**Conceptual Guide: Human-in-the-loop**](human_in_the_loop.md): Read the human-in-the-loop guide for more context on integrating human feedback into LangGraph applications using breakpoints.
|
||||
- [**How to View and Update Past Graph State**](../how-tos/human_in_the_loop/time-travel.ipynb): Step-by-step instructions for working with graph state that demonstrate the **replay** and **fork** actions.
|
||||
@@ -1,322 +1,744 @@
|
||||
# Human-in-the-loop
|
||||
|
||||
Human-in-the-loop (or "on-the-loop") enhances agent capabilities through several common user interaction patterns.
|
||||
!!! tip "This guide uses the new `interrupt` function."
|
||||
|
||||
Common interaction patterns include:
|
||||
As of LangGraph 0.2.57, the recommended way to set breakpoints is using the [`interrupt` function][langgraph.types.interrupt] as it simplifies **human-in-the-loop** patterns.
|
||||
|
||||
(1) `Approval` - We can interrupt our agent, surface the current state to a user, and allow the user to accept an action.
|
||||
If you're looking for the previous version of this conceptual guide, which relied on static breakpoints and `NodeInterrupt` exception, it is available [here](v0-human-in-the-loop.md).
|
||||
|
||||
(2) `Editing` - We can interrupt our agent, surface the current state to a user, and allow the user to edit the agent state.
|
||||
A **human-in-the-loop** (or "on-the-loop") workflow integrates human input into automated processes, allowing for decisions, validation, or corrections at key stages. This is especially useful in **LLM-based applications**, where the underlying model may generate occasional inaccuracies. In low-error-tolerance scenarios like compliance, decision-making, or content generation, human involvement ensures reliability by enabling review, correction, or override of model outputs.
|
||||
|
||||
(3) `Input` - We can explicitly create a graph node to collect human input and pass that input directly to the agent state.
|
||||
|
||||
Use-cases for these interaction patterns include:
|
||||
## Use cases
|
||||
|
||||
(1) `Reviewing tool calls` - We can interrupt an agent to review and edit the results of tool calls.
|
||||
Key use cases for **human-in-the-loop** workflows in LLM-based applications include:
|
||||
|
||||
(2) `Time Travel` - We can manually re-play and / or fork past actions of an agent.
|
||||
1. [**🛠️ Reviewing tool calls**](#review-tool-calls): Humans can review, edit, or approve tool calls requested by the LLM before tool execution.
|
||||
2. **✅ Validating LLM outputs**: Humans can review, edit, or approve content generated by the LLM.
|
||||
3. **💡 Providing context**: Enable the LLM to explicitly request human input for clarification or additional details or to support multi-turn conversations.
|
||||
|
||||
## Persistence
|
||||
## `interrupt`
|
||||
|
||||
All of these interaction patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which will write a checkpoint of the graph state at each step. Persistence allows the graph to stop so that a human can review and / or edit the current state of the graph and then resume with the human's input.
|
||||
|
||||
### Breakpoints
|
||||
|
||||
Adding a [breakpoint](./low_level.md#breakpoints) a specific location in the graph flow is one way to enable human-in-the-loop. In this case, the developer knows *where* in the workflow human input is needed and simply places a breakpoint prior to or following that particular graph node.
|
||||
|
||||
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
|
||||
The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. This function is useful for tasks like approvals, edits, or collecting additional input. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`](../reference/types.md#langgraph.types.Command) object to resume the graph with a value provided by the human.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["step_for_human_in_the_loop"])
|
||||
from langgraph.types import interrupt
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
thread_config = {"configurable": {"thread_id": "1"}}
|
||||
for event in graph.stream(inputs, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
def human_node(state: State):
|
||||
value = interrupt(
|
||||
# Any JSON serializable value to surface to the human.
|
||||
# For example, a question or a piece of text or a set of keys in the state
|
||||
{
|
||||
"text_to_revise": state["some_text"]
|
||||
}
|
||||
)
|
||||
# Update the state with the human's input or route the graph based on the input.
|
||||
return {
|
||||
"some_text": value
|
||||
}
|
||||
|
||||
graph = graph_builder.compile(
|
||||
checkpointer=checkpointer # Required for `interrupt` to work
|
||||
)
|
||||
|
||||
# Run the graph until the interrupt
|
||||
thread_config = {"configurable": {"thread_id": "some_id"}}
|
||||
graph.invoke(some_input, config=thread_config)
|
||||
|
||||
# Perform some action that requires human in the loop
|
||||
|
||||
# Continue the graph execution from the current checkpoint
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
# Resume the graph with the human's input
|
||||
graph.invoke(Command(resume=value_from_human), config=thread_config)
|
||||
```
|
||||
|
||||
### Dynamic Breakpoints
|
||||
```pycon
|
||||
{'some_text': 'Edited text'}
|
||||
```
|
||||
|
||||
Alternatively, the developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
!!! warning
|
||||
Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used.
|
||||
For this reason, interrupts are typically best placed at the start of a node or in a dedicated node. Please read the [resuming from an interrupt](#how-does-resuming-from-an-interrupt-work) section for more details.
|
||||
|
||||
??? "Full Code"
|
||||
|
||||
Here's a full example of how to use `interrupt` in a graph, if you'd like
|
||||
to see the code in action.
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
class State(TypedDict):
|
||||
"""The graph state."""
|
||||
some_text: str
|
||||
|
||||
def human_node(state: State):
|
||||
value = interrupt(
|
||||
# Any JSON serializable value to surface to the human.
|
||||
# For example, a question or a piece of text or a set of keys in the state
|
||||
{
|
||||
"text_to_revise": state["some_text"]
|
||||
}
|
||||
)
|
||||
return {
|
||||
# Update the state with the human's input
|
||||
"some_text": value
|
||||
}
|
||||
|
||||
|
||||
# Build the graph
|
||||
graph_builder = StateGraph(State)
|
||||
# Add the human-node to the graph
|
||||
graph_builder.add_node("human_node", human_node)
|
||||
graph_builder.add_edge(START, "human_node")
|
||||
|
||||
# A checkpointer is required for `interrupt` to work.
|
||||
checkpointer = MemorySaver()
|
||||
graph = graph_builder.compile(
|
||||
checkpointer=checkpointer
|
||||
)
|
||||
|
||||
# Pass a thread ID to the graph to run it.
|
||||
thread_config = {"configurable": {"thread_id": uuid.uuid4()}}
|
||||
|
||||
# Using stream() to directly surface the `__interrupt__` information.
|
||||
for chunk in graph.stream({"some_text": "Original text"}, config=thread_config):
|
||||
print(chunk)
|
||||
|
||||
# Resume using Command
|
||||
for chunk in graph.stream(Command(resume="Edited text"), config=thread_config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'__interrupt__': (
|
||||
Interrupt(
|
||||
value={'question': 'Please revise the text', 'some_text': 'Original text'},
|
||||
resumable=True,
|
||||
ns=['human_node:10fe492f-3688-c8c6-0d0a-ec61a43fecd6'],
|
||||
when='during'
|
||||
),
|
||||
)
|
||||
}
|
||||
{'human_node': {'some_text': 'Edited text'}}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
To use `interrupt` in your graph, you need to:
|
||||
|
||||
1. [**Specify a checkpointer**](persistence.md#checkpoints) to save the graph state after each step.
|
||||
2. **Call `interrupt()`** in the appropriate place. See the [Design Patterns](#design-patterns) section for examples.
|
||||
3. **Run the graph** with a [**thread ID**](./persistence.md#threads) until the `interrupt` is hit.
|
||||
4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](#the-command-primitive)).
|
||||
|
||||
## Design Patterns
|
||||
|
||||
There are typically three different **actions** that you can do with a human-in-the-loop workflow:
|
||||
|
||||
1. **Approve or Reject**: Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. This pattern often involve **routing** the graph based on the human's input.
|
||||
2. **Edit Graph State**: Pause the graph to review and edit the graph state. This is useful for correcting mistakes or updating the state with additional information. This pattern often involves **updating** the state with the human's input.
|
||||
3. **Get Input**: Explicitly request human input at a particular step in the graph. This is useful for collecting additional information or context to inform the agent's decision-making process or for supporting **multi-turn conversations**.
|
||||
|
||||
Below we show different design patterns that can be implemented using these **actions**.
|
||||
|
||||
### Approve or Reject
|
||||
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
<figcaption>Depending on the human's approval or rejection, the graph can proceed with the action or take an alternative path.</figcaption>
|
||||
</figure>
|
||||
|
||||
Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
return state
|
||||
|
||||
from typing import Literal
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
def human_approval(state: State) -> Command[Literal["some_node", "another_node"]]:
|
||||
is_approved = interrupt(
|
||||
{
|
||||
"question": "Is this correct?",
|
||||
# Surface the output that should be
|
||||
# reviewed and approved by the human.
|
||||
"llm_output": state["llm_output"]
|
||||
}
|
||||
)
|
||||
|
||||
if is_approved:
|
||||
return Command(goto="some_node")
|
||||
else:
|
||||
return Command(goto="another_node")
|
||||
|
||||
# Add the node to the graph in an appropriate location
|
||||
# and connect it to the relevant nodes.
|
||||
graph_builder.add_node("human_approval", human_approval)
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# After running the graph and hitting the interrupt, the graph will pause.
|
||||
# Resume it with either an approval or rejection.
|
||||
thread_config = {"configurable": {"thread_id": "some_id"}}
|
||||
graph.invoke(Command(resume=True), config=thread_config)
|
||||
```
|
||||
|
||||
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
|
||||
See [how to review tool calls](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a more detailed example.
|
||||
|
||||
### Review & Edit State
|
||||
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
<figcaption>A human can review and edit the state of the graph. This is useful for correcting mistakes or updating the state with additional information.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```python
|
||||
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_editing(state: State):
|
||||
...
|
||||
result = interrupt(
|
||||
# Interrupt information to surface to the client.
|
||||
# Can be any JSON serializable value.
|
||||
{
|
||||
"task": "Review the output from the LLM and make any necessary edits.",
|
||||
"llm_generated_summary": state["llm_generated_summary"]
|
||||
}
|
||||
)
|
||||
|
||||
# Update the state with the edited text
|
||||
return {
|
||||
"llm_generated_summary": result["edited_text"]
|
||||
}
|
||||
|
||||
# Add the node to the graph in an appropriate location
|
||||
# and connect it to the relevant nodes.
|
||||
graph_builder.add_node("human_editing", human_editing)
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
...
|
||||
|
||||
# After running the graph and hitting the interrupt, the graph will pause.
|
||||
# Resume it with the edited text.
|
||||
thread_config = {"configurable": {"thread_id": "some_id"}}
|
||||
graph.invoke(
|
||||
Command(resume={"edited_text": "The edited text"}),
|
||||
config=thread_config
|
||||
)
|
||||
```
|
||||
|
||||
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
|
||||
See [How to wait for user input using interrupt](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a more detailed example.
|
||||
|
||||
```python
|
||||
# Update the state to pass the dynamic breakpoint
|
||||
graph.update_state(config=thread_config, values={"input": "foo"})
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
### Review Tool Calls
|
||||
|
||||
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
<figcaption>A human can review and edit the output from the LLM before proceeding. This is particularly
|
||||
critical in applications where the tool calls requested by the LLM may be sensitive or require human oversight.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```python
|
||||
# This update will skip the node `my_node` altogether
|
||||
graph.update_state(config=thread_config, values=None, as_node="my_node")
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]:
|
||||
# This is the value we'll be providing via Command(resume=<human_review>)
|
||||
human_review = interrupt(
|
||||
{
|
||||
"question": "Is this correct?",
|
||||
# Surface tool calls for review
|
||||
"tool_call": tool_call
|
||||
}
|
||||
)
|
||||
|
||||
review_action, review_data = human_review
|
||||
|
||||
# Approve the tool call and continue
|
||||
if review_action == "continue":
|
||||
return Command(goto="run_tool")
|
||||
|
||||
# Modify the tool call manually and then continue
|
||||
elif review_action == "update":
|
||||
...
|
||||
updated_msg = get_updated_msg(review_data)
|
||||
# Remember that to modify an existing message you will need
|
||||
# to pass the message with a matching ID.
|
||||
return Command(goto="run_tool", update={"messages": [updated_message]})
|
||||
|
||||
# Give natural language feedback, and then pass that back to the agent
|
||||
elif review_action == "feedback":
|
||||
...
|
||||
feedback_msg = get_feedback_msg(review_data)
|
||||
return Command(goto="call_llm", update={"messages": [feedback_msg]})
|
||||
```
|
||||
|
||||
See [our guide](../how-tos/human_in_the_loop/dynamic_breakpoints.ipynb) for a detailed how-to on doing this!
|
||||
See [how to review tool calls](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a more detailed example.
|
||||
|
||||
## Interaction Patterns
|
||||
### Multi-turn conversation
|
||||
|
||||
### Approval
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
<figcaption>A <strong>multi-turn conversation</strong> architecture where an <strong>agent</strong> and <strong>human node</strong> cycle back and forth until the agent decides to hand off the conversation to another agent or another part of the system.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||

|
||||
A **multi-turn conversation** involves multiple back-and-forth interactions between an agent and a human, which can allow the agent to gather additional information from the human in a conversational manner.
|
||||
|
||||
Sometimes we want to approve certain steps in our agent's execution.
|
||||
|
||||
We can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to the step that we want to approve.
|
||||
This design pattern is useful in an LLM application consisting of [multiple agents](./multi_agent.md). One or more agents may need to carry out multi-turn conversations with a human, where the human provides input or feedback at different stages of the conversation. For simplicity, the agent implementation below is illustrated as a single node, but in reality
|
||||
it may be part of a larger graph consisting of multiple nodes and include a conditional edge.
|
||||
|
||||
This is generally recommend for sensitive actions (e.g., using external APIs or writing to a database).
|
||||
|
||||
With persistence, we can surface the current agent state as well as the next step to a user for review and approval.
|
||||
|
||||
If approved, the graph resumes execution from the last saved checkpoint, which is saved to the `thread`:
|
||||
=== "Using a human node per agent"
|
||||
|
||||
In this pattern, each agent has its own human node for collecting user input.
|
||||
This can be achieved by either naming the human nodes with unique names (e.g., "human for agent 1", "human for agent 2") or by
|
||||
using subgraphs where a subgraph contains a human node and an agent node.
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_input(state: State):
|
||||
human_message = interrupt("human_input")
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": human_message
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def agent(state: State):
|
||||
# Agent logic
|
||||
...
|
||||
|
||||
graph_builder.add_node("human_input", human_input)
|
||||
graph_builder.add_edge("human_input", "agent")
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# After running the graph and hitting the interrupt, the graph will pause.
|
||||
# Resume it with the human's input.
|
||||
graph.invoke(
|
||||
Command(resume="hello!"),
|
||||
config=thread_config
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
=== "Sharing human node across multiple agents"
|
||||
|
||||
In this pattern, a single human node is used to collect user input for multiple agents. The active agent is determined from the state, so after human input is collected, the graph can route to the correct agent.
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_node(state: MessagesState) -> Command[Literal["agent_1", "agent_2", ...]]:
|
||||
"""A node for collecting user input."""
|
||||
user_input = interrupt(value="Ready for user input.")
|
||||
|
||||
# Determine the **active agent** from the state, so
|
||||
# we can route to the correct agent after collecting input.
|
||||
# For example, add a field to the state or use the last active agent.
|
||||
# or fill in `name` attribute of AI messages generated by the agents.
|
||||
active_agent = ...
|
||||
|
||||
return Command(
|
||||
update={
|
||||
"messages": [{
|
||||
"role": "human",
|
||||
"content": user_input,
|
||||
}]
|
||||
},
|
||||
goto=active_agent,
|
||||
)
|
||||
```
|
||||
|
||||
See [how to implement multi-turn conversations](../how-tos/multi-agent-multi-turn-convo.ipynb) for a more detailed example.
|
||||
|
||||
### Validating human input
|
||||
|
||||
If you need to validate the input provided by the human within the graph itself (rather than on the client side), you can achieve this by using multiple interrupt calls within a single node.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to approve
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
|
||||
from langgraph.types import interrupt
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# ... Get human approval ...
|
||||
def human_node(state: State):
|
||||
"""Human node with validation."""
|
||||
question = "What is your age?"
|
||||
|
||||
# If approved, continue the graph execution from the last saved checkpoint
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
while True:
|
||||
answer = interrupt(question)
|
||||
|
||||
# Validate answer, if the answer isn't valid ask for input again.
|
||||
if not isinstance(answer, int) or answer < 0:
|
||||
question = f"'{answer} is not a valid age. What is your age?"
|
||||
answer = None
|
||||
continue
|
||||
else:
|
||||
# If the answer is valid, we can proceed.
|
||||
break
|
||||
|
||||
print(f"The human in the loop is {answer} years old.")
|
||||
return {
|
||||
"age": answer
|
||||
}
|
||||
```
|
||||
|
||||
See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed how-to on doing this!
|
||||
## The `Command` primitive
|
||||
|
||||
### Editing
|
||||
When using the `interrupt` function, the graph will pause at the interrupt and wait for user input.
|
||||
|
||||

|
||||
Graph execution can be resumed using the [Command](../reference/types.md#langgraph.types.Command) primitive which can be passed through the `invoke`, `ainvoke`, `stream` or `astream` methods.
|
||||
|
||||
Sometimes we want to review and edit the agent's state.
|
||||
|
||||
As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to the step we want to check.
|
||||
|
||||
We can surface the current state to a user and allow the user to edit the agent state.
|
||||
|
||||
This can, for example, be used to correct the agent if it made a mistake (e.g., see the section on tool calling below).
|
||||
The `Command` primitive provides several options to control and modify the graph's state during resumption:
|
||||
|
||||
We can edit the graph state by forking the current checkpoint, which is saved to the `thread`.
|
||||
1. **Pass a value to the `interrupt`**: Provide data, such as a user's response, to the graph using `Command(resume=value)`. Execution resumes from the beginning of the node where the `interrupt` was used, however, this time the `interrupt(...)` call will return the value passed in the `Command(resume=value)` instead of pausing the graph.
|
||||
|
||||
We can then proceed with the graph from our forked checkpoint as done before.
|
||||
```python
|
||||
# Resume graph execution with the user's input.
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
2. **Update the graph state**: Modify the graph state using `Command(update=update)`. Note that resumption starts from the beginning of the node where the `interrupt` was used. Execution resumes from the beginning of the node where the `interrupt` was used, but with the updated state.
|
||||
|
||||
```python
|
||||
# Update the graph state and resume.
|
||||
# You must provide a `resume` value if using an `interrupt`.
|
||||
graph.invoke(Command(update={"foo": "bar"}, resume="Let's go!!!"), thread_config)
|
||||
```
|
||||
|
||||
By leveraging `Command`, you can resume graph execution, handle user inputs, and dynamically adjust the graph's state.
|
||||
|
||||
## Using with `invoke` and `ainvoke`
|
||||
|
||||
When you use `stream` or `astream` to run the graph, you will receive an `Interrupt` event that let you know the `interrupt` was triggered.
|
||||
|
||||
`invoke` and `ainvoke` do not return the interrupt information. To access this information, you must use the [get_state](../reference/graphs.md#langgraph.graph.graph.CompiledGraph.get_state) method to retrieve the graph state after calling `invoke` or `ainvoke`.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to review
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Review the state, decide to edit it, and create a forked checkpoint with the new state
|
||||
graph.update_state(thread, {"state": "new state"})
|
||||
|
||||
# Continue the graph execution from the forked checkpoint
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
# Run the graph up to the interrupt
|
||||
result = graph.invoke(inputs, thread_config)
|
||||
# Get the graph state to get interrupt information.
|
||||
state = graph.get_state(thread_config)
|
||||
# Print the state values
|
||||
print(state.values)
|
||||
# Print the pending tasks
|
||||
print(state.tasks)
|
||||
# Resume the graph with the user's input.
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/edit-graph-state.ipynb) for a detailed how-to on doing this!
|
||||
```pycon
|
||||
{'foo': 'bar'} # State values
|
||||
(
|
||||
PregelTask(
|
||||
id='5d8ffc92-8011-0c9b-8b59-9d3545b7e553',
|
||||
name='node_foo',
|
||||
path=('__pregel_pull', 'node_foo'),
|
||||
error=None,
|
||||
interrupts=(Interrupt(value='value_in_interrupt', resumable=True, ns=['node_foo:5d8ffc92-8011-0c9b-8b59-9d3545b7e553'], when='during'),), state=None,
|
||||
result=None
|
||||
),
|
||||
) # Pending tasks. interrupts
|
||||
```
|
||||
|
||||
### Input
|
||||
## How does resuming from an interrupt work?
|
||||
|
||||

|
||||
!!! warning
|
||||
|
||||
Sometimes we want to explicitly get human input at a particular step in the graph.
|
||||
|
||||
We can create a graph node designated for this (e.g., `human_input` in our example diagram).
|
||||
|
||||
As with approval and editing, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to this node.
|
||||
|
||||
We can then perform a state update that includes the human input, just as we did with editing state.
|
||||
Resuming from an `interrupt` is **different** from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called.
|
||||
|
||||
But, we add one thing:
|
||||
A critical aspect of using `interrupt` is understanding how resuming works. When you resume execution after an `interrupt`, graph execution starts from the **beginning** of the **graph node** where the last `interrupt` was triggered.
|
||||
|
||||
We can use `as_node=human_input` with the state update to specify that the state update *should be treated as a node*.
|
||||
|
||||
The is subtle, but important:
|
||||
|
||||
With editing, the user makes a decision about whether or not to edit the graph state.
|
||||
|
||||
With input, we explicitly define a node in our graph for collecting human input!
|
||||
|
||||
The state update with the human input then runs *as this node*.
|
||||
**All** code from the beginning of the node to the `interrupt` will be re-executed.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to to collect human input
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_input"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Update the state with the user input as if it was the human_input node
|
||||
graph.update_state(thread, {"user_input": user_input}, as_node="human_input")
|
||||
|
||||
# Continue the graph execution from the checkpoint created by the human_input node
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
counter = 0
|
||||
def node(state: State):
|
||||
# All the code from the beginning of the node to the interrupt will be re-executed
|
||||
# when the graph resumes.
|
||||
global counter
|
||||
counter += 1
|
||||
print(f"> Entered the node: {counter} # of times")
|
||||
# Pause the graph and wait for user input.
|
||||
answer = interrupt()
|
||||
print("The value of counter is:", counter)
|
||||
...
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a detailed how-to on doing this!
|
||||
Upon **resuming** the graph, the counter will be incremented a second time, resulting in the following output:
|
||||
|
||||
## Use-cases
|
||||
```pycon
|
||||
> Entered the node: 2 # of times
|
||||
The value of counter is: 2
|
||||
```
|
||||
|
||||
### Reviewing Tool Calls
|
||||
## Common Pitfalls
|
||||
|
||||
Some user interaction patterns combine the above ideas.
|
||||
### Side-effects
|
||||
|
||||
For example, many agents use [tool calling](https://python.langchain.com/docs/how_to/tool_calling/) to make decisions.
|
||||
Place code with side effects, such as API calls, **after** the `interrupt` to avoid duplication, as these are re-triggered every time the node is resumed.
|
||||
|
||||
Tool calling presents a challenge because the agent must get two things right:
|
||||
=== "Side effects before interrupt (BAD)"
|
||||
|
||||
(1) The name of the tool to call
|
||||
This code will re-execute the API call another time when the node is resumed from
|
||||
the `interrupt`.
|
||||
|
||||
(2) The arguments to pass to the tool
|
||||
This can be problematic if the API call is not idempotent or is just expensive.
|
||||
|
||||
Even if the tool call is correct, we may also want to apply discretion:
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
(3) The tool call may be a sensitive operation that we want to approve
|
||||
def human_node(state: State):
|
||||
"""Human node with validation."""
|
||||
api_call(...) # This code will be re-executed when the node is resumed.
|
||||
answer = interrupt(question)
|
||||
```
|
||||
|
||||
With these points in mind, we can combine the above ideas to create a human-in-the-loop review of a tool call.
|
||||
=== "Side effects after interrupt (OK)"
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_node(state: State):
|
||||
"""Human node with validation."""
|
||||
|
||||
answer = interrupt(question)
|
||||
|
||||
api_call(answer) # OK as it's after the interrupt
|
||||
```
|
||||
|
||||
=== "Side effects in a separate node (OK)"
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_node(state: State):
|
||||
"""Human node with validation."""
|
||||
|
||||
answer = interrupt(question)
|
||||
|
||||
return {
|
||||
"answer": answer
|
||||
}
|
||||
|
||||
def api_call_node(state: State):
|
||||
api_call(...) # OK as it's in a separate node
|
||||
```
|
||||
|
||||
### Subgraphs called as functions
|
||||
|
||||
When invoking a subgraph [as a function](low_level.md#as-a-function), the **parent graph** will resume execution from the **beginning of the node** where the subgraph was invoked (and where an `interrupt` was triggered). Similarly, the **subgraph**, will resume from the **beginning of the node** where the `interrupt()` function was called.
|
||||
|
||||
For example,
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to to review the tool call from the LLM
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_review"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Review the tool call and update it, if needed, as the human_review node
|
||||
graph.update_state(thread, {"tool_call": "updated tool call"}, as_node="human_review")
|
||||
|
||||
# Otherwise, approve the tool call and proceed with the graph execution with no edits
|
||||
|
||||
# Continue the graph execution from either:
|
||||
# (1) the forked checkpoint created by human_review or
|
||||
# (2) the checkpoint saved when the tool call was originally made (no edits in human_review)
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
def node_in_parent_graph(state: State):
|
||||
some_code() # <-- This will re-execute when the subgraph is resumed.
|
||||
# Invoke a subgraph as a function.
|
||||
# The subgraph contains an `interrupt` call.
|
||||
subgraph_result = subgraph.invoke(some_input)
|
||||
...
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a detailed how-to on doing this!
|
||||
??? "**Example: Parent and Subgraph Execution Flow**"
|
||||
|
||||
### Time Travel
|
||||
Say we have a parent graph with 3 nodes:
|
||||
|
||||
When working with agents, we often want closely examine their decision making process:
|
||||
**Parent Graph**: `node_1` → `node_2` (subgraph call) → `node_3`
|
||||
|
||||
(1) Even when they arrive a desired final result, the reasoning that led to that result is often important to examine.
|
||||
And the subgraph has 3 nodes, where the second node contains an `interrupt`:
|
||||
|
||||
(2) When agents make mistakes, it is often valuable to understand why.
|
||||
**Subgraph**: `sub_node_1` → `sub_node_2` (`interrupt`) → `sub_node_3`
|
||||
|
||||
(3) In either of the above cases, it is useful to manually explore alternative decision making paths.
|
||||
When resuming the graph, the execution will proceed as follows:
|
||||
|
||||
Collectively, we call these debugging concepts `time-travel` and they are composed of `replaying` and `forking`.
|
||||
1. **Skip `node_1`** in the parent graph (already executed, graph state was saved in snapshot).
|
||||
2. **Re-execute `node_2`** in the parent graph from the start.
|
||||
3. **Skip `sub_node_1`** in the subgraph (already executed, graph state was saved in snapshot).
|
||||
4. **Re-execute `sub_node_2`** in the subgraph from the beginning.
|
||||
5. Continue with `sub_node_3` and subsequent nodes.
|
||||
|
||||
#### Replaying
|
||||
Here is abbreviated example code that you can use to understand how subgraphs work with interrupts.
|
||||
It counts the number of times each node is entered and prints the count.
|
||||
|
||||

|
||||
```python
|
||||
import uuid
|
||||
from typing import TypedDict
|
||||
|
||||
Sometimes we want to simply replay past actions of an agent.
|
||||
|
||||
Above, we showed the case of executing an agent from the current state (or checkpoint) of the graph.
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.constants import START
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
We by simply passing in `None` for the input with a `thread`.
|
||||
|
||||
```
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
class State(TypedDict):
|
||||
"""The graph state."""
|
||||
state_counter: int
|
||||
|
||||
Now, we can modify this to replay past actions from a *specific* checkpoint by passing in the checkpoint ID.
|
||||
|
||||
To get a specific checkpoint ID, we can easily get all of the checkpoints in the thread and filter to the one we want.
|
||||
counter_node_in_subgraph = 0
|
||||
|
||||
```python
|
||||
all_checkpoints = []
|
||||
for state in app.get_state_history(thread):
|
||||
all_checkpoints.append(state)
|
||||
```
|
||||
def node_in_subgraph(state: State):
|
||||
"""A node in the sub-graph."""
|
||||
global counter_node_in_subgraph
|
||||
counter_node_in_subgraph += 1 # This code will **NOT** run again!
|
||||
print(f"Entered `node_in_subgraph` a total of {counter_node_in_subgraph} times")
|
||||
|
||||
Each checkpoint has a unique ID, which we can use to replay from a specific checkpoint.
|
||||
counter_human_node = 0
|
||||
|
||||
Assume from reviewing the checkpoints that we want to replay from one, `xxx`.
|
||||
def human_node(state: State):
|
||||
global counter_human_node
|
||||
counter_human_node += 1 # This code will run again!
|
||||
print(f"Entered human_node in sub-graph a total of {counter_human_node} times")
|
||||
answer = interrupt("what is your name?")
|
||||
print(f"Got an answer of {answer}")
|
||||
|
||||
We just pass in the checkpoint ID when we run the graph.
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
Importantly, the graph knows which checkpoints have been previously executed.
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
So, it will re-play any previously executed nodes rather than re-executing them.
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node("some_node", node_in_subgraph)
|
||||
subgraph_builder.add_node("human_node", human_node)
|
||||
subgraph_builder.add_edge(START, "some_node")
|
||||
subgraph_builder.add_edge("some_node", "human_node")
|
||||
subgraph = subgraph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay) for related context on replaying.
|
||||
|
||||
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
|
||||
counter_parent_node = 0
|
||||
|
||||
#### Forking
|
||||
def parent_node(state: State):
|
||||
"""This parent node will invoke the subgraph."""
|
||||
global counter_parent_node
|
||||
|
||||

|
||||
counter_parent_node += 1 # This code will run again on resuming!
|
||||
print(f"Entered `parent_node` a total of {counter_parent_node} times")
|
||||
|
||||
# Please note that we're intentionally incrementing the state counter
|
||||
# in the graph state as well to demonstrate that the subgraph update
|
||||
# of the same key will not conflict with the parent graph (until
|
||||
subgraph_state = subgraph.invoke(state)
|
||||
return subgraph_state
|
||||
|
||||
Sometimes we want to fork past actions of an agent, and explore different paths through the graph.
|
||||
|
||||
`Editing`, as discussed above, is *exactly* how we do this for the *current* state of the graph!
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("parent_node", parent_node)
|
||||
builder.add_edge(START, "parent_node")
|
||||
|
||||
But, what if we want to fork *past* states of the graph?
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
For example, let's say we want to edit a particular checkpoint, `xxx`.
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": uuid.uuid4(),
|
||||
}
|
||||
}
|
||||
|
||||
We pass this `checkpoint_id` when we update the state of the graph.
|
||||
for chunk in graph.stream({"state_counter": 1}, config):
|
||||
print(chunk)
|
||||
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xxx"}}
|
||||
graph.update_state(config, {"state": "updated state"}, )
|
||||
```
|
||||
print('--- Resuming ---')
|
||||
|
||||
This creates a new forked checkpoint, `xxx-fork`, which we can then run the graph from.
|
||||
for chunk in graph.stream(Command(resume="35"), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx-fork'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
This will print out
|
||||
|
||||
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state) for related context on forking.
|
||||
```pycon
|
||||
--- First invocation ---
|
||||
In parent node: {'foo': 'bar'}
|
||||
Entered `parent_node` a total of 1 times
|
||||
Entered `node_in_subgraph` a total of 1 times
|
||||
Entered human_node in sub-graph a total of 1 times
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:0b23d72f-aaba-0329-1a59-ca4f3c8bad3b', 'human_node:25df717c-cb80-57b0-7410-44e20aac8f3c'], when='during'),)}
|
||||
|
||||
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
|
||||
--- Resuming ---
|
||||
In parent node: {'foo': 'bar'}
|
||||
Entered `parent_node` a total of 2 times
|
||||
Entered human_node in sub-graph a total of 2 times
|
||||
Got an answer of 35
|
||||
{'parent_node': None}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Using multiple interrupts
|
||||
|
||||
Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validating-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully.
|
||||
|
||||
When a node contains multiple interrupt calls, LangGraph keeps a list of resume values specific to the task executing the node. Whenever execution resumes, it starts at the beginning of the node. For each interrupt encountered, LangGraph checks if a matching value exists in the task's resume list. Matching is **strictly index-based**, so the order of interrupt calls within the node is critical.
|
||||
|
||||
To avoid issues, refrain from dynamically changing the node's structure between executions. This includes adding, removing, or reordering interrupt calls, as such changes can result in mismatched indices. These problems often arise from unconventional patterns, such as mutating state via `Command(resume=..., update=SOME_STATE_MUTATION)` or relying on global variables to modify the node’s structure dynamically.
|
||||
|
||||
??? "Example of incorrect code"
|
||||
|
||||
```python
|
||||
import uuid
|
||||
from typing import TypedDict, Optional
|
||||
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.constants import START
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
"""The graph state."""
|
||||
|
||||
age: Optional[str]
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
def human_node(state: State):
|
||||
if not state.get('name'):
|
||||
name = interrupt("what is your name?")
|
||||
else:
|
||||
name = "N/A"
|
||||
|
||||
if not state.get('age'):
|
||||
age = interrupt("what is your age?")
|
||||
else:
|
||||
age = "N/A"
|
||||
|
||||
print(f"Name: {name}. Age: {age}")
|
||||
|
||||
return {
|
||||
"age": age,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("human_node", human_node)
|
||||
builder.add_edge(START, "human_node")
|
||||
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": uuid.uuid4(),
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in graph.stream({"age": None, "name": None}, config):
|
||||
print(chunk)
|
||||
|
||||
for chunk in graph.stream(Command(resume="John", update={"name": "foo"}), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)}
|
||||
Name: N/A. Age: John
|
||||
{'human_node': {'age': 'John', 'name': 'N/A'}}
|
||||
```
|
||||
|
||||
## Additional Resources 📚
|
||||
|
||||
- [**Conceptual Guide: Persistence**](persistence.md#replay): Read the persistence guide for more context on replaying.
|
||||
- [**How to Guides: Human-in-the-loop**](../how-tos/index.md#human-in-the-loop): Learn how to implement human-in-the-loop workflows in LangGraph.
|
||||
- [**How to implement multi-turn conversations**](../how-tos/multi-agent-multi-turn-convo.ipynb): Learn how to implement multi-turn conversations in LangGraph.
|
||||
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -24,7 +24,9 @@ The conceptual guide does not cover step-by-step instructions or specific implem
|
||||
- [LangGraph Glossary](low_level.md): LangGraph workflows are designed as graphs, with nodes representing different components and edges representing the flow of information between them. This guide provides an overview of the key concepts associated with LangGraph graph primitives.
|
||||
- [Common Agentic Patterns](agentic_concepts.md): An agent uses an LLM to pick its own control flow to solve more complex problems! Agents are a key building block in many LLM applications. This guide explains the different types of agent architectures and how they can be used to control the flow of an application.
|
||||
- [Multi-Agent Systems](multi_agent.md): Complex LLM applications can often be broken down into multiple agents, each responsible for a different part of the application. This guide explains common patterns for building multi-agent systems.
|
||||
- [Breakpoints](breakpoints.md): Breakpoints allow pausing the execution of a graph at specific points. Breakpoints allow stepping through graph execution for debugging purposes.
|
||||
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
|
||||
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
|
||||
- [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.
|
||||
|
||||
@@ -33,6 +33,11 @@ The `langgraph build` command builds a Docker image for the [LangGraph API serve
|
||||
!!! note "New in version 0.1.55"
|
||||
The `langgraph dev` command was introduced in langgraph-cli version 0.1.55.
|
||||
|
||||
!!! note "Python only"
|
||||
|
||||
Currently, the CLI only supports Python >= 3.11.
|
||||
JS support is coming soon.
|
||||
|
||||
The `langgraph dev` command starts a lightweight development server that requires no Docker installation. This server is ideal for rapid development and testing, with features like:
|
||||
|
||||
- Hot reloading: Changes to your code are automatically detected and reloaded
|
||||
|
||||
@@ -21,6 +21,18 @@ See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for c
|
||||
| Development | 1 CPU | 1 GB | Up to 1 container |
|
||||
| Production | 1 CPU | 2 GB | Up to 10 containers |
|
||||
|
||||
## Autoscaling
|
||||
`Production` type deployments automatically scale up to 10 containers. Scaling is based on the current request load for a single container. Specifically, the autoscaling implementation scales the deployment so that each container is processing about 10 concurrent requests. For example...
|
||||
|
||||
- If the deployment is processing 20 concurrent requests, the deployment will scale up from 1 container to 2 containers (20 requests / 2 containers = 10 requests per container).
|
||||
- If a deployment of 2 containers is processing 10 requests, the deployment will scale down from 2 containers to 1 container (10 requests / 1 container = 10 requests per container).
|
||||
|
||||
10 concurrent requests per container is the target threshold. However, 10 concurrent requests per container is not a hard limit. The number of concurrent requests can exceed 10 if there is a sudden burst of requests.
|
||||
|
||||
Scale down actions are delayed for 30 minutes before any action is taken. In other words, if the autoscaling implementation decides to scale down a deployment, it will first wait for 30 minutes before scaling down. After 30 minutes, the concurrency metric is recomputed and the deployment will scale down if the concurrency metric has met the target threshold. Otherwise, the deployment remains scaled up. This "cool down" period ensures that deployments do not scale up and down too frequently.
|
||||
|
||||
In the future, the autoscaling implementation may evolve to accommodate other metrics such as background run queue size.
|
||||
|
||||
## Revision
|
||||
|
||||
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
|
||||
@@ -31,6 +43,12 @@ See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for cre
|
||||
|
||||
Infrastructure for [deployments](#deployment) and [revisions](#revision) are provisioned and deployed asynchronously. They are not deployed immediately after submission. Currently, deployment can take up to several minutes.
|
||||
|
||||
- When a new deployment is created, a new database is created for the deployment. Database creation is a one-time step. This step contributes to a longer deployment time for the initial revision of the deployment.
|
||||
- When a subsequent revision is created for a deployment, there is no database creation step. The deployment time for a subsequent revision is significantly faster compared to the deployment time of the initial revision.
|
||||
- The deployment process for each revision contains a build step, which can take up to a few minutes.
|
||||
|
||||
!!! info "Database creation for `Development` type deployments takes longer than database creation for `Production` type deployments."
|
||||
|
||||
## Architecture
|
||||
|
||||
!!! warning "Subject to Change"
|
||||
@@ -40,7 +58,6 @@ A high-level diagram of a Cloud SaaS deployment.
|
||||
|
||||

|
||||
|
||||
|
||||
## Related
|
||||
|
||||
- [Deployment Options](./deployment_options.md)
|
||||
|
||||
@@ -339,37 +339,6 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
)
|
||||
```
|
||||
|
||||
`Command` has the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| `graph` | Graph to send the command to. Supported values:<br>- `None`: the current graph (default)<br>- `Command.PARENT`: closest parent graph |
|
||||
| `update` | Update to apply to the graph's state. |
|
||||
| `resume` | Value to resume execution with. To be used together with [`interrupt()`][langgraph.types.interrupt]. |
|
||||
| `goto` | Can be one of the following:<br>- name of the node to navigate to next (any node that belongs to the specified `graph`)<br>- sequence of node names to navigate to next<br>- `Send` object (to execute a node with the input provided)<br>- sequence of `Send` objects<br>If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START
|
||||
from langgraph.types import Command
|
||||
from typing_extensions import Literal, TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
return Command(update={"foo": "bar"}, goto="my_other_node")
|
||||
|
||||
def my_other_node(state: State):
|
||||
return {"foo": state["foo"] + "baz"}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_edge(START, "my_node")
|
||||
builder.add_node("my_node", my_node)
|
||||
builder.add_node("my_other_node", my_other_node)
|
||||
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
|
||||
|
||||
```python
|
||||
@@ -380,10 +349,44 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
|
||||
!!! important
|
||||
|
||||
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["node_b", "node_c"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`.
|
||||
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["my_other_node"]]`. This is necessary for the graph rendering and tells LangGraph that `my_node` can navigate to `my_other_node`.
|
||||
|
||||
Check out this [how-to guide](../how-tos/command.ipynb) for an end-to-end example of how to use `Command`.
|
||||
|
||||
### When should I use Command instead of conditional edges?
|
||||
|
||||
Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](./multi_agent.md#handoffs) where it's important to route to a different agent and pass some information to that agent.
|
||||
|
||||
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
|
||||
|
||||
### Using inside tools
|
||||
|
||||
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool:
|
||||
|
||||
```python
|
||||
@tool
|
||||
def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):
|
||||
"""Use this to look up user information to better assist them with their questions."""
|
||||
user_info = get_user_info(config.get("configurable", {}).get("user_id"))
|
||||
return Command(
|
||||
update={
|
||||
# update the state keys
|
||||
"user_info": user_info,
|
||||
# update the message history
|
||||
"messages": [ToolMessage("Successfully looked up user information", tool_call_id=tool_call_id)]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
!!! important
|
||||
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
|
||||
|
||||
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `Command(resume="User input")`. Check out [this conceptual guide](./human_in_the_loop.md) for more information.
|
||||
|
||||
## Persistence
|
||||
|
||||
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
|
||||
@@ -449,35 +452,32 @@ graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthr
|
||||
|
||||
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
|
||||
|
||||
## `interrupt`
|
||||
|
||||
Use the [interrupt](../reference/types.md/#langgraph.types.interrupt) function to **pause** the graph at specific points to collect user input. The `interrupt` function surfaces interrupt information to the client, allowing the developer to collect user input, validate the graph state, or make decisions before resuming execution.
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def human_approval_node(state: State):
|
||||
...
|
||||
answer = interrupt(
|
||||
# This value will be sent to the client.
|
||||
# It can be any JSON serializable value.
|
||||
{"question": "is it ok to continue?"},
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
Resuming the graph is done by passing a [`Command`](#command) object to the graph with the `resume` key set to the value returned by the `interrupt` function.
|
||||
|
||||
Read more about how the `interrupt` is used for **human-in-the-loop** workflows in the [Human-in-the-loop conceptual guide](./human_in_the_loop.md).
|
||||
|
||||
## Breakpoints
|
||||
|
||||
It can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you ["compile" a graph](#compiling-your-graph). You can set breakpoints either _before_ a node executes (using `interrupt_before`) or after a node executes (using `interrupt_after`.)
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
|
||||
|
||||
You **MUST** use a [checkpointer](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
|
||||
|
||||
In order to resume execution, you can just invoke your graph with `None` as the input.
|
||||
|
||||
```python
|
||||
# Initial run of graph
|
||||
graph.invoke(inputs, config=config)
|
||||
|
||||
# Let's assume it hit a breakpoint somewhere, you can then resume by passing in None
|
||||
graph.invoke(None, config=config)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a full walkthrough of how to add breakpoints.
|
||||
|
||||
### Dynamic Breakpoints
|
||||
|
||||
It may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. In `LangGraph` you can do so by using `NodeInterrupt` -- a special exception that can be raised from inside a node.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
|
||||
return state
|
||||
```
|
||||
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
|
||||
|
||||
## Subgraphs
|
||||
|
||||
@@ -518,7 +518,7 @@ The simplest way to create subgraph nodes is by using a [compiled subgraph](#com
|
||||
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.graph import StateGraph
|
||||
from typing import TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
|
||||
@@ -26,13 +26,88 @@ There are several ways to connect agents in a multi-agent system:
|
||||
- **Hierarchical**: you can define a multi-agent system with [a supervisor of supervisors](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/). This is a generalization of the supervisor architecture and allows for more complex control flows.
|
||||
- **Custom multi-agent workflow**: each agent communicates with only a subset of agents. Parts of the flow are deterministic, and only some agents can decide which other agents to call next.
|
||||
|
||||
### Handoffs
|
||||
|
||||
In multi-agent architectures, agents can be represented as graph nodes. Each agent node executes its step(s) and decides whether to finish execution or route to another agent, including potentially routing to itself (e.g., running in a loop). A common pattern in multi-agent interactions is handoffs, where one agent hands off control to another. Handoffs allow you to specify:
|
||||
|
||||
- __destination__: target agent to navigate to (e.g., name of the node to go to)
|
||||
- __payload__: [information to pass to that agent](#communication-between-agents) (e.g., state update)
|
||||
|
||||
To implement handoffs in LangGraph, agent nodes can return [`Command`](./low_level.md#command) object that allows you to combine both control flow and state updates:
|
||||
|
||||
```python
|
||||
def agent(state) -> Command[Literal["agent", "another_agent"]]:
|
||||
# the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
|
||||
goto = get_next_agent(...) # 'agent' / 'another_agent'
|
||||
return Command(
|
||||
# Specify which agent to call next
|
||||
goto=goto,
|
||||
# Update the graph state
|
||||
update={"my_state_key": "my_state_value"}
|
||||
)
|
||||
```
|
||||
|
||||
In a more complex scenario where each agent node is itself a graph (i.e., a [subgraph](./low_level.md#subgraphs)), a node in one of the agent subgraphs might want to navigate to a different agent. For example, if you have two agents, `alice` and `bob` (subgraph nodes in a parent graph), and `alice` needs to navigate to `bob`, you can set `graph=Command.PARENT` in the `Command` object:
|
||||
|
||||
```python
|
||||
def some_node_inside_alice(state)
|
||||
return Command(
|
||||
goto="bob",
|
||||
update={"my_state_key": "my_state_value"},
|
||||
# specify which graph to navigate to (defaults to the current graph)
|
||||
graph=Command.PARENT,
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you need to support visualization for subgraphs communicating using `Command(graph=Command.PARENT)` you would need to wrap them in a node function with `Command` annotation, e.g. instead of this:
|
||||
|
||||
```python
|
||||
builder.add_node(alice)
|
||||
```
|
||||
|
||||
you would need to do this:
|
||||
|
||||
```python
|
||||
def call_alice(state) -> Command[Literal["bob"]]:
|
||||
return alice.invoke(state)
|
||||
|
||||
builder.add_node("alice", call_alice)
|
||||
```
|
||||
|
||||
#### Handoffs as tools
|
||||
|
||||
One of the most common agent types is a ReAct-style tool-calling agents. For those types of agents, a common pattern is wrapping a handoff in a tool call, e.g.:
|
||||
|
||||
```python
|
||||
def transfer_to_bob(state):
|
||||
"""Transfer to bob."""
|
||||
return Command(
|
||||
goto="bob",
|
||||
update={"my_state_key": "my_state_value"},
|
||||
graph=Command.PARENT,
|
||||
)
|
||||
```
|
||||
|
||||
This is a special case of updating the graph state from tools where in addition the state update, the control flow is included as well.
|
||||
|
||||
!!! important
|
||||
|
||||
If you want to use tools that return `Command`, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:
|
||||
|
||||
```python
|
||||
def call_tools(state):
|
||||
...
|
||||
commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls]
|
||||
return commands
|
||||
```
|
||||
|
||||
Let's now take a closer look at the different multi-agent architectures.
|
||||
|
||||
### Network
|
||||
|
||||
In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. This architecture is good for problems that do not have a clear hierarchy of agents or a specific sequence in which agents should be called.
|
||||
|
||||
### Supervisor
|
||||
|
||||
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [conditional edges](./low_level.md#conditional-edges) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
@@ -41,39 +116,83 @@ from langgraph.graph import StateGraph, MessagesState, START
|
||||
|
||||
model = ChatOpenAI()
|
||||
|
||||
class AgentState(MessagesState):
|
||||
next: Literal["agent_1", "agent_2", "__end__"]
|
||||
|
||||
def supervisor(state: AgentState):
|
||||
def agent_1(state: MessagesState) -> Command[Literal["agent_2", "agent_3", END]]:
|
||||
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
|
||||
# to determine which agent to call next. a common pattern is to call the model
|
||||
# with a structured output (e.g. force it to return an output with a "next_agent" field)
|
||||
response = model.invoke(...)
|
||||
# the "next" key will be used by the conditional edges to route execution
|
||||
# to the appropriate agent
|
||||
return {"next": response["next_agent"]}
|
||||
# route to one of the agents or exit based on the LLM's decision
|
||||
# if the LLM returns "__end__", the graph will finish execution
|
||||
return Command(
|
||||
goto=response["next_agent"],
|
||||
update={"messages": [response["content"]]},
|
||||
)
|
||||
|
||||
def agent_1(state: AgentState):
|
||||
def agent_2(state: MessagesState) -> Command[Literal["agent_1", "agent_3", END]]:
|
||||
response = model.invoke(...)
|
||||
return Command(
|
||||
goto=response["next_agent"],
|
||||
update={"messages": [response["content"]]},
|
||||
)
|
||||
|
||||
def agent_3(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
|
||||
...
|
||||
return Command(
|
||||
goto=response["next_agent"],
|
||||
update={"messages": [response["content"]]},
|
||||
)
|
||||
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(agent_1)
|
||||
builder.add_node(agent_2)
|
||||
builder.add_node(agent_3)
|
||||
|
||||
builder.add_edge(START, "agent_1")
|
||||
network = builder.compile()
|
||||
```
|
||||
|
||||
### Supervisor
|
||||
|
||||
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](./low_level.md#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
|
||||
model = ChatOpenAI()
|
||||
|
||||
def supervisor(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
|
||||
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
|
||||
# to determine which agent to call next. a common pattern is to call the model
|
||||
# with a structured output (e.g. force it to return an output with a "next_agent" field)
|
||||
response = model.invoke(...)
|
||||
# route to one of the agents or exit based on the supervisor's decision
|
||||
# if the supervisor returns "__end__", the graph will finish execution
|
||||
return Command(goto=response["next_agent"])
|
||||
|
||||
def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]:
|
||||
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
|
||||
# and add any additional logic (different models, custom prompts, structured output, etc.)
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
return Command(
|
||||
goto="supervisor",
|
||||
update={"messages": [response]},
|
||||
)
|
||||
|
||||
def agent_2(state: AgentState):
|
||||
def agent_2(state: MessagesState) -> Command[Literal["supervisor"]]:
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
return Command(
|
||||
goto="supervisor",
|
||||
update={"messages": [response]},
|
||||
)
|
||||
|
||||
builder = StateGraph(AgentState)
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(supervisor)
|
||||
builder.add_node(agent_1)
|
||||
builder.add_node(agent_2)
|
||||
|
||||
builder.add_edge(START, "supervisor")
|
||||
# route to one of the agents or exit based on the supervisor's decisiion
|
||||
# if the supervisor returns "__end__", the graph will finish execution
|
||||
builder.add_conditional_edges("supervisor", lambda state: state["next"])
|
||||
builder.add_edge("agent_1", "supervisor")
|
||||
builder.add_edge("agent_2", "supervisor")
|
||||
|
||||
supervisor = builder.compile()
|
||||
```
|
||||
@@ -121,37 +240,29 @@ To address this, you can design your system _hierarchically_. For example, you c
|
||||
```python
|
||||
from typing import Literal
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
|
||||
model = ChatOpenAI()
|
||||
|
||||
# define team 1 (same as the single supervisor example above)
|
||||
class Team1State(MessagesState):
|
||||
next: Literal["team_1_agent_1", "team_1_agent_2", "__end__"]
|
||||
|
||||
def team_1_supervisor(state: Team1State):
|
||||
def team_1_supervisor(state: MessagesState) -> Command[Literal["team_1_agent_1", "team_1_agent_2", END]]:
|
||||
response = model.invoke(...)
|
||||
return {"next": response["next_agent"]}
|
||||
return Command(goto=response["next_agent"])
|
||||
|
||||
def team_1_agent_1(state: Team1State):
|
||||
def team_1_agent_1(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
return Command(goto="team_1_supervisor", update={"messages": [response]})
|
||||
|
||||
def team_1_agent_2(state: Team1State):
|
||||
def team_1_agent_2(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
return Command(goto="team_1_supervisor", update={"messages": [response]})
|
||||
|
||||
team_1_builder = StateGraph(Team1State)
|
||||
team_1_builder.add_node(team_1_supervisor)
|
||||
team_1_builder.add_node(team_1_agent_1)
|
||||
team_1_builder.add_node(team_1_agent_2)
|
||||
team_1_builder.add_edge(START, "team_1_supervisor")
|
||||
# route to one of the agents or exit based on the supervisor's decisiion
|
||||
# if the supervisor returns "__end__", the graph will finish execution
|
||||
team_1_builder.add_conditional_edges("team_1_supervisor", lambda state: state["next"])
|
||||
team_1_builder.add_edge("team_1_agent_1", "team_1_supervisor")
|
||||
team_1_builder.add_edge("team_1_agent_2", "team_1_supervisor")
|
||||
|
||||
team_1_graph = team_1_builder.compile()
|
||||
|
||||
# define team 2 (same as the single supervisor example above)
|
||||
@@ -174,31 +285,22 @@ team_2_graph = team_2_builder.compile()
|
||||
|
||||
# define top-level supervisor
|
||||
|
||||
class TopLevelState(MessagesState):
|
||||
next: Literal["team_1", "team_2", "__end__"]
|
||||
|
||||
builder = StateGraph(TopLevelState)
|
||||
def top_level_supervisor(state: TopLevelState):
|
||||
builder = StateGraph(MessagesState)
|
||||
def top_level_supervisor(state: MessagesState):
|
||||
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
|
||||
# to determine which team to call next. a common pattern is to call the model
|
||||
# with a structured output (e.g. force it to return an output with a "next_team" field)
|
||||
response = model.invoke(...)
|
||||
# the "next" key will be used by the conditional edges to route execution
|
||||
# to the appropriate team
|
||||
return {"next": response["next_team"]}
|
||||
# route to one of the teams or exit based on the supervisor's decision
|
||||
# if the supervisor returns "__end__", the graph will finish execution
|
||||
return Command(goto=response["next_team"])
|
||||
|
||||
builder = StateGraph(TopLevelState)
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(top_level_supervisor)
|
||||
builder.add_node(team_1_graph)
|
||||
builder.add_node(team_2_graph)
|
||||
|
||||
builder.add_edge(START, "top_level_supervisor")
|
||||
# route to one of the teams or exit based on the supervisor's decision
|
||||
# if the top-level supervisor returns "__end__", the graph will finish execution
|
||||
builder.add_conditional_edges("top_level_supervisor", lambda state: state["next"])
|
||||
builder.add_edge("team_1_graph", "top_level_supervisor")
|
||||
builder.add_edge("team_2_graph", "top_level_supervisor")
|
||||
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
@@ -208,7 +310,7 @@ In this architecture we add individual agents as graph nodes and define the orde
|
||||
|
||||
- **Explicit control flow (normal edges)**: LangGraph allows you to explicitly define the control flow of your application (i.e. the sequence of how agents communicate) explicitly, via [normal graph edges](./low_level.md#normal-edges). This is the most deterministic variant of this architecture above — we always know which agent will be called next ahead of time.
|
||||
|
||||
- **Dynamic control flow (conditional edges)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [conditional edges](./low_level.md#conditional-edges). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called.
|
||||
- **Dynamic control flow (Command)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [`Command`](./low_level.md#command). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called.
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
@@ -168,7 +168,7 @@ Importantly, LangGraph knows whether a particular checkpoint has been executed p
|
||||
|
||||
### Update state
|
||||
|
||||
In addition to re-playing the graph from specific `checkpoints`, we can also *edit* the graph state. We do this using `graph.update_state()`. This method three different arguments:
|
||||
In addition to re-playing the graph from specific `checkpoints`, we can also *edit* the graph state. We do this using `graph.update_state()`. This method accepts three different arguments:
|
||||
|
||||
#### `config`
|
||||
|
||||
@@ -222,7 +222,7 @@ The final thing you can optionally specify when calling `update_state` is `as_no
|
||||
|
||||
A [state schema](low_level.md#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence.
|
||||
|
||||
But, what if we want to retrain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
|
||||
But, what if we want to retain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
|
||||
|
||||
With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](../reference/store.md#langgraph.store.base.BaseStore) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
|
||||
|
||||
@@ -471,7 +471,7 @@ Second, checkpointers allow for ["memory"](agentic_concepts.md#memory) between i
|
||||
|
||||
### Time Travel
|
||||
|
||||
Third, checkpointers allow for ["time travel"](../how-tos/human_in_the_loop/time-travel.ipynb), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories.
|
||||
Third, checkpointers allow for ["time travel"](time-travel.md), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories.
|
||||
|
||||
### Fault-tolerance
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
# Template Applications
|
||||
|
||||
!!! note Prerequisites
|
||||
|
||||
- [LangGraph Studio](./langgraph_studio.md)
|
||||
|
||||
Templates are open source reference applications designed to help you get started quickly when building with LangGraph. They provide working examples of common agentic workflows that can be customized to your needs.
|
||||
|
||||
Templates can be accessed via [LangGraph Studio (macOS only)](langgraph_studio.md), or cloned directly from Github. You can download LangGraph Studio and see available templates [here](https://studio.langchain.com/).
|
||||
You can create an application from a template using the LangGraph CLI.
|
||||
|
||||
## Available templates
|
||||
!!! info "Requirements"
|
||||
|
||||
- Python >= 3.11
|
||||
- [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
|
||||
|
||||
## Install the LangGraph CLI
|
||||
|
||||
```bash
|
||||
pip install "langgraph-cli[inmem]" --upgrade
|
||||
```
|
||||
|
||||
## Available Templates
|
||||
|
||||
| Template | Description | Python | JS/TS |
|
||||
|---------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
@@ -17,3 +24,39 @@ Templates can be accessed via [LangGraph Studio (macOS only)](langgraph_studio.m
|
||||
| **Memory Agent** | A ReAct-style agent with an additional tool to store memories for use across threads. | [Repo](https://github.com/langchain-ai/memory-agent) | [Repo](https://github.com/langchain-ai/memory-agent-js) |
|
||||
| **Retrieval Agent** | An agent that includes a retrieval-based question-answering system. | [Repo](https://github.com/langchain-ai/retrieval-agent-template) | [Repo](https://github.com/langchain-ai/retrieval-agent-template-js) |
|
||||
| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment) | [Repo](https://github.com/langchain-ai/data-enrichment-js) |
|
||||
|
||||
|
||||
## 🌱 Create a LangGraph App
|
||||
|
||||
To create a new app from a template, use the `langgraph new` command.
|
||||
|
||||
```bash
|
||||
langgraph new
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Review the `README.md` file in the root of your new LangGraph app for more information about the template and how to customize it.
|
||||
|
||||
After configuring the app properly and adding your API keys, you can start the app using the LangGraph CLI:
|
||||
|
||||
```bash
|
||||
langgraph dev
|
||||
```
|
||||
|
||||
See the following guides for more information on how to deploy your app:
|
||||
|
||||
- **[Launch Local LangGraph Server](../tutorials/langgraph-platform/local-server.md)**: This quick start guide shows how to start a LangGraph Server locally for the **ReAct Agent** template. The steps are similar for other templates.
|
||||
- **[Deploy to LangGraph Cloud](../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
|
||||
|
||||
### LangGraph Framework
|
||||
|
||||
- **[LangGraph Concepts](../concepts/index.md)**: Learn the foundational concepts of LangGraph.
|
||||
- **[LangGraph How-to Guides](../how-tos/index.md)**: Guides for common tasks with LangGraph.
|
||||
|
||||
### 📚 Learn More about LangGraph Platform
|
||||
|
||||
Expand your knowledge with these resources:
|
||||
|
||||
- **[LangGraph Platform Concepts](../concepts/index.md#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform.
|
||||
- **[LangGraph Platform How-to Guides](../how-tos/index.md#langgraph-platform)**: Discover step-by-step guides to build and deploy applications.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Time Travel ⏱️
|
||||
|
||||
!!! note "Prerequisites"
|
||||
|
||||
This guide assumes that you are familiar with LangGraph's checkpoints and states. If not, please review the [persistence](./persistence.md) concept first.
|
||||
|
||||
|
||||
When working with non-deterministic systems that make model-based decisions (e.g., agents powered by LLMs), it can be useful to examine their decision-making process in detail:
|
||||
|
||||
1. 🤔 **Understand Reasoning**: Analyze the steps that led to a successful result.
|
||||
2. 🐞 **Debug Mistakes**: Identify where and why errors occurred.
|
||||
3. 🔍 **Explore Alternatives**: Test different paths to uncover better solutions.
|
||||
|
||||
We call these debugging techniques **Time Travel**, composed of two key actions: [**Replaying**](#replaying) 🔁 and [**Forking**](#forking) 🔀 .
|
||||
|
||||
## Replaying
|
||||
|
||||

|
||||
|
||||
Replaying allows us to revisit and reproduce an agent's past actions. This can be done either from the current state (or checkpoint) of the graph or from a specific checkpoint.
|
||||
|
||||
To replay from the current state, simply pass `None` as the input along with a `thread`:
|
||||
|
||||
```python
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
To replay actions from a specific checkpoint, start by retrieving all checkpoints for the thread:
|
||||
|
||||
```python
|
||||
all_checkpoints = []
|
||||
for state in graph.get_state_history(thread):
|
||||
all_checkpoints.append(state)
|
||||
```
|
||||
|
||||
Each checkpoint has a unique ID. After identifying the desired checkpoint, for instance, `xyz`, include its ID in the configuration:
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xyz'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
The graph efficiently replays previously executed nodes instead of re-executing them, leveraging its awareness of prior checkpoint executions.
|
||||
|
||||
## Forking
|
||||
|
||||

|
||||
|
||||
Forking allows you to revisit an agent's past actions and explore alternative paths within the graph.
|
||||
|
||||
To edit a specific checkpoint, such as `xyz`, provide its `checkpoint_id` when updating the graph's state:
|
||||
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xyz"}}
|
||||
graph.update_state(config, {"state": "updated state"})
|
||||
```
|
||||
|
||||
This creates a new forked checkpoint, xyz-fork, from which you can continue running the graph:
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xyz-fork'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
## Additional Resources 📚
|
||||
|
||||
- [**Conceptual Guide: Persistence**](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay): Read the persistence guide for more context on replaying.
|
||||
- [**How to View and Update Past Graph State**](../how-tos/human_in_the_loop/time-travel.ipynb): Step-by-step instructions for working with graph state that demonstrate the **replay** and **fork** actions.
|
||||
@@ -0,0 +1,329 @@
|
||||
# Human-in-the-loop
|
||||
|
||||
!!! note "Use the `interrupt` function instead."
|
||||
|
||||
As of LangGraph 0.2.57, the recommended way to set breakpoints is using the [`interrupt` function][langgraph.types.interrupt] as it simplifies **human-in-the-loop** patterns.
|
||||
|
||||
Please see the revised [human-in-the-loop guide](./human_in_the_loop.md) for the latest version that uses the `interrupt` function.
|
||||
|
||||
|
||||
Human-in-the-loop (or "on-the-loop") enhances agent capabilities through several common user interaction patterns.
|
||||
|
||||
Common interaction patterns include:
|
||||
|
||||
(1) `Approval` - We can interrupt our agent, surface the current state to a user, and allow the user to accept an action.
|
||||
|
||||
(2) `Editing` - We can interrupt our agent, surface the current state to a user, and allow the user to edit the agent state.
|
||||
|
||||
(3) `Input` - We can explicitly create a graph node to collect human input and pass that input directly to the agent state.
|
||||
|
||||
Use-cases for these interaction patterns include:
|
||||
|
||||
(1) `Reviewing tool calls` - We can interrupt an agent to review and edit the results of tool calls.
|
||||
|
||||
(2) `Time Travel` - We can manually re-play and / or fork past actions of an agent.
|
||||
|
||||
## Persistence
|
||||
|
||||
All of these interaction patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which will write a checkpoint of the graph state at each step. Persistence allows the graph to stop so that a human can review and / or edit the current state of the graph and then resume with the human's input.
|
||||
|
||||
### Breakpoints
|
||||
|
||||
Adding a [breakpoint](./breakpoints.md) a specific location in the graph flow is one way to enable human-in-the-loop. In this case, the developer knows *where* in the workflow human input is needed and simply places a breakpoint prior to or following that particular graph node.
|
||||
|
||||
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["step_for_human_in_the_loop"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
thread_config = {"configurable": {"thread_id": "1"}}
|
||||
for event in graph.stream(inputs, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Perform some action that requires human in the loop
|
||||
|
||||
# Continue the graph execution from the current checkpoint
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
### Dynamic Breakpoints
|
||||
|
||||
Alternatively, the developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./breakpoints.md) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
return state
|
||||
```
|
||||
|
||||
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
|
||||
|
||||
```python
|
||||
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
|
||||
|
||||
```python
|
||||
# Update the state to pass the dynamic breakpoint
|
||||
graph.update_state(config=thread_config, values={"input": "foo"})
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
|
||||
|
||||
```python
|
||||
# This update will skip the node `my_node` altogether
|
||||
graph.update_state(config=thread_config, values=None, as_node="my_node")
|
||||
for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [our guide](../how-tos/human_in_the_loop/dynamic_breakpoints.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
## Interaction Patterns
|
||||
|
||||
### Approval
|
||||
|
||||

|
||||
|
||||
Sometimes we want to approve certain steps in our agent's execution.
|
||||
|
||||
We can interrupt our agent at a [breakpoint](./breakpoints.md) prior to the step that we want to approve.
|
||||
|
||||
This is generally recommend for sensitive actions (e.g., using external APIs or writing to a database).
|
||||
|
||||
With persistence, we can surface the current agent state as well as the next step to a user for review and approval.
|
||||
|
||||
If approved, the graph resumes execution from the last saved checkpoint, which is saved to the `thread`:
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to approve
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# ... Get human approval ...
|
||||
|
||||
# If approved, continue the graph execution from the last saved checkpoint
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
### Editing
|
||||
|
||||

|
||||
|
||||
Sometimes we want to review and edit the agent's state.
|
||||
|
||||
As with approval, we can interrupt our agent at a [breakpoint](./breakpoints.md) prior to the step we want to check.
|
||||
|
||||
We can surface the current state to a user and allow the user to edit the agent state.
|
||||
|
||||
This can, for example, be used to correct the agent if it made a mistake (e.g., see the section on tool calling below).
|
||||
|
||||
We can edit the graph state by forking the current checkpoint, which is saved to the `thread`.
|
||||
|
||||
We can then proceed with the graph from our forked checkpoint as done before.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to review
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Review the state, decide to edit it, and create a forked checkpoint with the new state
|
||||
graph.update_state(thread, {"state": "new state"})
|
||||
|
||||
# Continue the graph execution from the forked checkpoint
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/edit-graph-state.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
### Input
|
||||
|
||||

|
||||
|
||||
Sometimes we want to explicitly get human input at a particular step in the graph.
|
||||
|
||||
We can create a graph node designated for this (e.g., `human_input` in our example diagram).
|
||||
|
||||
As with approval and editing, we can interrupt our agent at a [breakpoint](./breakpoints.md) prior to this node.
|
||||
|
||||
We can then perform a state update that includes the human input, just as we did with editing state.
|
||||
|
||||
But, we add one thing:
|
||||
|
||||
We can use `as_node=human_input` with the state update to specify that the state update *should be treated as a node*.
|
||||
|
||||
The is subtle, but important:
|
||||
|
||||
With editing, the user makes a decision about whether or not to edit the graph state.
|
||||
|
||||
With input, we explicitly define a node in our graph for collecting human input!
|
||||
|
||||
The state update with the human input then runs *as this node*.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to to collect human input
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_input"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Update the state with the user input as if it was the human_input node
|
||||
graph.update_state(thread, {"user_input": user_input}, as_node="human_input")
|
||||
|
||||
# Continue the graph execution from the checkpoint created by the human_input node
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
## Use-cases
|
||||
|
||||
### Reviewing Tool Calls
|
||||
|
||||
Some user interaction patterns combine the above ideas.
|
||||
|
||||
For example, many agents use [tool calling](https://python.langchain.com/docs/how_to/tool_calling/) to make decisions.
|
||||
|
||||
Tool calling presents a challenge because the agent must get two things right:
|
||||
|
||||
(1) The name of the tool to call
|
||||
|
||||
(2) The arguments to pass to the tool
|
||||
|
||||
Even if the tool call is correct, we may also want to apply discretion:
|
||||
|
||||
(3) The tool call may be a sensitive operation that we want to approve
|
||||
|
||||
With these points in mind, we can combine the above ideas to create a human-in-the-loop review of a tool call.
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before the step to to review the tool call from the LLM
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_review"])
|
||||
|
||||
# Run the graph up to the breakpoint
|
||||
for event in graph.stream(inputs, thread, stream_mode="values"):
|
||||
print(event)
|
||||
|
||||
# Review the tool call and update it, if needed, as the human_review node
|
||||
graph.update_state(thread, {"tool_call": "updated tool call"}, as_node="human_review")
|
||||
|
||||
# Otherwise, approve the tool call and proceed with the graph execution with no edits
|
||||
|
||||
# Continue the graph execution from either:
|
||||
# (1) the forked checkpoint created by human_review or
|
||||
# (2) the checkpoint saved when the tool call was originally made (no edits in human_review)
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
### Time Travel
|
||||
|
||||
When working with agents, we often want closely examine their decision making process:
|
||||
|
||||
(1) Even when they arrive a desired final result, the reasoning that led to that result is often important to examine.
|
||||
|
||||
(2) When agents make mistakes, it is often valuable to understand why.
|
||||
|
||||
(3) In either of the above cases, it is useful to manually explore alternative decision making paths.
|
||||
|
||||
Collectively, we call these debugging concepts `time-travel` and they are composed of `replaying` and `forking`.
|
||||
|
||||
#### Replaying
|
||||
|
||||

|
||||
|
||||
Sometimes we want to simply replay past actions of an agent.
|
||||
|
||||
Above, we showed the case of executing an agent from the current state (or checkpoint) of the graph.
|
||||
|
||||
We by simply passing in `None` for the input with a `thread`.
|
||||
|
||||
```
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
for event in graph.stream(None, thread, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
Now, we can modify this to replay past actions from a *specific* checkpoint by passing in the checkpoint ID.
|
||||
|
||||
To get a specific checkpoint ID, we can easily get all of the checkpoints in the thread and filter to the one we want.
|
||||
|
||||
```python
|
||||
all_checkpoints = []
|
||||
for state in app.get_state_history(thread):
|
||||
all_checkpoints.append(state)
|
||||
```
|
||||
|
||||
Each checkpoint has a unique ID, which we can use to replay from a specific checkpoint.
|
||||
|
||||
Assume from reviewing the checkpoints that we want to replay from one, `xxx`.
|
||||
|
||||
We just pass in the checkpoint ID when we run the graph.
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
Importantly, the graph knows which checkpoints have been previously executed.
|
||||
|
||||
So, it will re-play any previously executed nodes rather than re-executing them.
|
||||
|
||||
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay) for related context on replaying.
|
||||
|
||||
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
|
||||
|
||||
#### Forking
|
||||
|
||||

|
||||
|
||||
Sometimes we want to fork past actions of an agent, and explore different paths through the graph.
|
||||
|
||||
`Editing`, as discussed above, is *exactly* how we do this for the *current* state of the graph!
|
||||
|
||||
But, what if we want to fork *past* states of the graph?
|
||||
|
||||
For example, let's say we want to edit a particular checkpoint, `xxx`.
|
||||
|
||||
We pass this `checkpoint_id` when we update the state of the graph.
|
||||
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xxx"}}
|
||||
graph.update_state(config, {"state": "updated state"}, )
|
||||
```
|
||||
|
||||
This creates a new forked checkpoint, `xxx-fork`, which we can then run the graph from.
|
||||
|
||||
```python
|
||||
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx-fork'}}
|
||||
for event in graph.stream(None, config, stream_mode="values"):
|
||||
print(event)
|
||||
```
|
||||
|
||||
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state) for related context on forking.
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
|
||||
@@ -25,7 +25,7 @@
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def my_node(state: State) -> Command[Literal[\"my_other_node\"]]:\n",
|
||||
" return GraphCommand(\n",
|
||||
" return Command(\n",
|
||||
" # state update\n",
|
||||
" update={\"foo\": \"bar\"},\n",
|
||||
" # control flow\n",
|
||||
@@ -144,7 +144,7 @@
|
||||
"id": "badc25eb-4876-482e-bb10-d763023cdaad",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../../concepts/low_level#conditional-edges) for routing! This is because control flow is defined with `GraphCommand` inside `node_a`."
|
||||
"We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../../concepts/low_level#conditional-edges) for routing! This is because control flow is defined with `Command` inside `node_a`."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -171,7 +171,7 @@
|
||||
"source": [
|
||||
"!!! important\n",
|
||||
"\n",
|
||||
" You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
|
||||
" You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph rendering and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
"source": [
|
||||
"# How to add breakpoints\n",
|
||||
"\n",
|
||||
"!!! tip \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following concepts:\n",
|
||||
"\n",
|
||||
" * [Breakpoints](../../../concepts/breakpoints)\n",
|
||||
" * [LangGraph Glossary](../../../concepts/low_level)\n",
|
||||
" \n",
|
||||
"\n",
|
||||
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions). \n",
|
||||
"\n",
|
||||
"Breakpoints are built on top of LangGraph [checkpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer), which save the graph's state after each node execution. Checkpoints are saved in [threads](https://langchain-ai.github.io/langgraph/concepts/low_level/#threads) that preserve graph state and can be accessed after a graph has finished execution. This allows for graph execution to pause at specific points, await human approval, and then resume execution from the last checkpoint.\n",
|
||||
@@ -467,7 +475,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.8"
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "ee54cde3-7e4d-43f4-b921-e7141ea0f19e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add dynamic breakpoints"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "607849c6-4b8c-4e06-ad9c-758bb5a08e86",
|
||||
"id": "b7d5f6a5-9e59-43e4-a4b6-8ada6dace691",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add dynamic breakpoints with `NodeInterrupt`\n",
|
||||
"\n",
|
||||
"!!! note\n",
|
||||
"\n",
|
||||
" For **human-in-the-loop** workflows use the new [`interrupt()`](../../../reference/types/#langgraph.types.interrupt) function for **human-in-the-loop** workflows. Please review the [Human-in-the-loop conceptual guide](../../../concepts/human_in_the_loop) for more information about design patterns with `interrupt`.\n",
|
||||
"\n",
|
||||
"!!! tip \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following concepts:\n",
|
||||
"\n",
|
||||
" * [Breakpoints](../../../concepts/breakpoints)\n",
|
||||
" * [LangGraph Glossary](../../../concepts/low_level)\n",
|
||||
" \n",
|
||||
"\n",
|
||||
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).\n",
|
||||
"\n",
|
||||
"In LangGraph you can add breakpoints before / after a node is executed. But oftentimes it may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. When doing so, it may also be helpful to include information about **why** that interrupt was raised.\n",
|
||||
"\n",
|
||||
"This guide shows how you can dynamically interrupt the graph using `NodeInterrupt` -- a special exception that can be raised from inside a node. Let's see it in action!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages"
|
||||
@@ -430,7 +438,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
"source": [
|
||||
"# How to edit graph state\n",
|
||||
"\n",
|
||||
"!!! tip \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" * [Human-in-the-loop](../../../concepts/human_in_the_loop)\n",
|
||||
" * [Breakpoints](../../../concepts/breakpoints)\n",
|
||||
" * [LangGraph Glossary](../../../concepts/low_level)\n",
|
||||
"\n",
|
||||
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). Manually updating the graph state a common HIL interaction pattern, allowing the human to edit actions (e.g., what tool is being called or how it is being called).\n",
|
||||
"\n",
|
||||
"We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state and then resume from that spot to continue. \n",
|
||||
@@ -554,7 +560,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.8"
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
"source": [
|
||||
"# How to view and update past graph state\n",
|
||||
"\n",
|
||||
"!!! tip \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following concepts:\n",
|
||||
"\n",
|
||||
" * [Time Travel](../../../concepts/time-travel)\n",
|
||||
" * [Breakpoints](../../../concepts/breakpoints)\n",
|
||||
" * [LangGraph Glossary](../../../concepts/low_level)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Once you start [checkpointing](../../persistence) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n",
|
||||
"\n",
|
||||
"1. You can surface a state during an interrupt to a user to let them accept an action.\n",
|
||||
@@ -589,7 +598,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -30,7 +30,7 @@ These how-to guides show how to achieve that controllability.
|
||||
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
|
||||
- [How to add cross-thread persistence to your graph](cross-thread-persistence.ipynb)
|
||||
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
|
||||
- [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb)
|
||||
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
|
||||
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
|
||||
|
||||
### Memory
|
||||
@@ -48,12 +48,24 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
|
||||
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
|
||||
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
|
||||
|
||||
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
|
||||
- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb)
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb)
|
||||
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb)
|
||||
|
||||
Key workflows:
|
||||
|
||||
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb): A basic example that shows how to implement a human-in-the-loop workflow in your graph using the `interrupt` function.
|
||||
- [How to review tool calls](human_in_the_loop/review-tool-calls.ipynb): Incorporate human-in-the-loop for reviewing/editing/accepting tool call requests before they executed using the `interrupt` function.
|
||||
|
||||
|
||||
Other methods:
|
||||
|
||||
- [How to add static breakpoints](human_in_the_loop/breakpoints.ipynb): Use for debugging purposes. For [**human-in-the-loop**](../concepts/human_in_the_loop.md) workflows, we recommend the [`interrupt` function][langgraph.types.interrupt] instead.
|
||||
- [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.
|
||||
|
||||
### 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.
|
||||
|
||||
- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb)
|
||||
- [How to review tool calls](human_in_the_loop/review-tool-calls.ipynb)
|
||||
|
||||
### Streaming
|
||||
|
||||
@@ -81,6 +93,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
- [How to handle tool calling errors](tool-calling-errors.ipynb)
|
||||
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
|
||||
- [How to pass config to tools](pass-config-to-tools.ipynb)
|
||||
- [How to update graph state from tools](update-state-from-tools.ipynb)
|
||||
- [How to handle large numbers of tools](many-tools.ipynb)
|
||||
|
||||
### Subgraphs
|
||||
@@ -91,6 +104,15 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
|
||||
|
||||
### Multi-agent
|
||||
|
||||
[Multi-agent systems](../concepts/multi_agent.md) are useful to break down complex LLM applications into multiple agents, each responsible for a different part of the application. These how-to guides show how to implement multi-agent systems in LangGraph:
|
||||
|
||||
- [How to build a multi-agent network](multi-agent-network.ipynb)
|
||||
- [How to add multi-turn conversation in a multi-agent application](multi-agent-multi-turn-convo.ipynb)
|
||||
|
||||
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
|
||||
|
||||
### State Management
|
||||
|
||||
- [How to use Pydantic model as state](state-model.ipynb)
|
||||
@@ -225,6 +247,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
|
||||
- [How to connect to a local deployment (Docker)](../cloud/how-tos/test_local_deployment.md)
|
||||
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
|
||||
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
|
||||
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.base import (\n",
|
||||
" WRITES_IDX_MAP,\n",
|
||||
" BaseCheckpointSaver,\n",
|
||||
" ChannelVersions,\n",
|
||||
" Checkpoint,\n",
|
||||
@@ -163,7 +164,7 @@
|
||||
"from redis import Redis\n",
|
||||
"from redis.asyncio import Redis as AsyncRedis\n",
|
||||
"\n",
|
||||
"REDIS_KEY_SEPARATOR = \":\"\n",
|
||||
"REDIS_KEY_SEPARATOR = \"$\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Utilities shared by both RedisSaver and AsyncRedisSaver\n",
|
||||
@@ -246,17 +247,6 @@
|
||||
" return keys\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _dump_writes(serde: SerializerProtocol, writes: tuple[str, Any]) -> list[dict]:\n",
|
||||
" \"\"\"Serialize pending writes.\"\"\"\n",
|
||||
" serialized_writes = []\n",
|
||||
" for channel, value in writes:\n",
|
||||
" type_, serialized_value = serde.dumps_typed(value)\n",
|
||||
" serialized_writes.append(\n",
|
||||
" {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
|
||||
" )\n",
|
||||
" return serialized_writes\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _load_writes(\n",
|
||||
" serde: SerializerProtocol, task_id_to_data: dict[tuple[str, str], dict]\n",
|
||||
") -> list[PendingWrite]:\n",
|
||||
@@ -413,7 +403,7 @@
|
||||
" config: RunnableConfig,\n",
|
||||
" writes: List[Tuple[str, Any]],\n",
|
||||
" task_id: str,\n",
|
||||
" ) -> RunnableConfig:\n",
|
||||
" ) -> None:\n",
|
||||
" \"\"\"Store intermediate writes linked to a checkpoint.\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
@@ -425,12 +415,23 @@
|
||||
" checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
|
||||
" checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
|
||||
"\n",
|
||||
" for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
|
||||
" for idx, (channel, value) in enumerate(writes):\n",
|
||||
" key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
|
||||
" thread_id,\n",
|
||||
" checkpoint_ns,\n",
|
||||
" checkpoint_id,\n",
|
||||
" task_id,\n",
|
||||
" WRITES_IDX_MAP.get(channel, idx),\n",
|
||||
" )\n",
|
||||
" self.conn.hset(key, mapping=data)\n",
|
||||
" return config\n",
|
||||
" type_, serialized_value = self.serde.dumps_typed(value)\n",
|
||||
" data = {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
|
||||
" if all(w[0] in WRITES_IDX_MAP for w in writes):\n",
|
||||
" # Use HSET which will overwrite existing values\n",
|
||||
" self.conn.hset(key, mapping=data)\n",
|
||||
" else:\n",
|
||||
" # Use HSETNX which will not overwrite existing values\n",
|
||||
" for field, value in data.items():\n",
|
||||
" self.conn.hsetnx(key, field, value)\n",
|
||||
"\n",
|
||||
" def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
|
||||
" \"\"\"Get a checkpoint tuple from Redis.\n",
|
||||
@@ -463,21 +464,8 @@
|
||||
" checkpoint_id\n",
|
||||
" or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
|
||||
" )\n",
|
||||
" writes_key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
|
||||
" )\n",
|
||||
" matching_keys = self.conn.keys(pattern=writes_key)\n",
|
||||
" parsed_keys = [\n",
|
||||
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
|
||||
" ]\n",
|
||||
" pending_writes = _load_writes(\n",
|
||||
" self.serde,\n",
|
||||
" {\n",
|
||||
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): self.conn.hgetall(key)\n",
|
||||
" for key, parsed_key in sorted(\n",
|
||||
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
|
||||
" )\n",
|
||||
" },\n",
|
||||
" pending_writes = self._load_pending_writes(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id\n",
|
||||
" )\n",
|
||||
" return _parse_redis_checkpoint_data(\n",
|
||||
" self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
|
||||
@@ -514,7 +502,37 @@
|
||||
" for key in keys:\n",
|
||||
" data = self.conn.hgetall(key)\n",
|
||||
" if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
|
||||
" yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
|
||||
" # load pending writes\n",
|
||||
" checkpoint_id = _parse_redis_checkpoint_key(key.decode())[\n",
|
||||
" \"checkpoint_id\"\n",
|
||||
" ]\n",
|
||||
" pending_writes = self._load_pending_writes(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id\n",
|
||||
" )\n",
|
||||
" yield _parse_redis_checkpoint_data(\n",
|
||||
" self.serde, key.decode(), data, pending_writes=pending_writes\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" def _load_pending_writes(\n",
|
||||
" self, thread_id: str, checkpoint_ns: str, checkpoint_id: str\n",
|
||||
" ) -> List[PendingWrite]:\n",
|
||||
" writes_key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
|
||||
" )\n",
|
||||
" matching_keys = self.conn.keys(pattern=writes_key)\n",
|
||||
" parsed_keys = [\n",
|
||||
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
|
||||
" ]\n",
|
||||
" pending_writes = _load_writes(\n",
|
||||
" self.serde,\n",
|
||||
" {\n",
|
||||
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): self.conn.hgetall(key)\n",
|
||||
" for key, parsed_key in sorted(\n",
|
||||
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
|
||||
" )\n",
|
||||
" },\n",
|
||||
" )\n",
|
||||
" return pending_writes\n",
|
||||
"\n",
|
||||
" def _get_checkpoint_key(\n",
|
||||
" self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
|
||||
@@ -637,7 +655,7 @@
|
||||
" config: RunnableConfig,\n",
|
||||
" writes: List[Tuple[str, Any]],\n",
|
||||
" task_id: str,\n",
|
||||
" ) -> RunnableConfig:\n",
|
||||
" ) -> None:\n",
|
||||
" \"\"\"Store intermediate writes linked to a checkpoint asynchronously.\n",
|
||||
"\n",
|
||||
" This method saves intermediate writes associated with a checkpoint to the database.\n",
|
||||
@@ -651,12 +669,23 @@
|
||||
" checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
|
||||
" checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
|
||||
"\n",
|
||||
" for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
|
||||
" for idx, (channel, value) in enumerate(writes):\n",
|
||||
" key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
|
||||
" thread_id,\n",
|
||||
" checkpoint_ns,\n",
|
||||
" checkpoint_id,\n",
|
||||
" task_id,\n",
|
||||
" WRITES_IDX_MAP.get(channel, idx),\n",
|
||||
" )\n",
|
||||
" await self.conn.hset(key, mapping=data)\n",
|
||||
" return config\n",
|
||||
" type_, serialized_value = self.serde.dumps_typed(value)\n",
|
||||
" data = {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
|
||||
" if all(w[0] in WRITES_IDX_MAP for w in writes):\n",
|
||||
" # Use HSET which will overwrite existing values\n",
|
||||
" await self.conn.hset(key, mapping=data)\n",
|
||||
" else:\n",
|
||||
" # Use HSETNX which will not overwrite existing values\n",
|
||||
" for field, value in data.items():\n",
|
||||
" await self.conn.hsetnx(key, field, value)\n",
|
||||
"\n",
|
||||
" async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
|
||||
" \"\"\"Get a checkpoint tuple from Redis asynchronously.\n",
|
||||
@@ -688,21 +717,8 @@
|
||||
" checkpoint_id\n",
|
||||
" or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
|
||||
" )\n",
|
||||
" writes_key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
|
||||
" )\n",
|
||||
" matching_keys = await self.conn.keys(pattern=writes_key)\n",
|
||||
" parsed_keys = [\n",
|
||||
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
|
||||
" ]\n",
|
||||
" pending_writes = _load_writes(\n",
|
||||
" self.serde,\n",
|
||||
" {\n",
|
||||
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): await self.conn.hgetall(key)\n",
|
||||
" for key, parsed_key in sorted(\n",
|
||||
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
|
||||
" )\n",
|
||||
" },\n",
|
||||
" pending_writes = await self._aload_pending_writes(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id\n",
|
||||
" )\n",
|
||||
" return _parse_redis_checkpoint_data(\n",
|
||||
" self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
|
||||
@@ -738,7 +754,36 @@
|
||||
" for key in keys:\n",
|
||||
" data = await self.conn.hgetall(key)\n",
|
||||
" if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
|
||||
" yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
|
||||
" checkpoint_id = _parse_redis_checkpoint_key(key.decode())[\n",
|
||||
" \"checkpoint_id\"\n",
|
||||
" ]\n",
|
||||
" pending_writes = await self._aload_pending_writes(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id\n",
|
||||
" )\n",
|
||||
" yield _parse_redis_checkpoint_data(\n",
|
||||
" self.serde, key.decode(), data, pending_writes=pending_writes\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" async def _aload_pending_writes(\n",
|
||||
" self, thread_id: str, checkpoint_ns: str, checkpoint_id: str\n",
|
||||
" ) -> List[PendingWrite]:\n",
|
||||
" writes_key = _make_redis_checkpoint_writes_key(\n",
|
||||
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
|
||||
" )\n",
|
||||
" matching_keys = await self.conn.keys(pattern=writes_key)\n",
|
||||
" parsed_keys = [\n",
|
||||
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
|
||||
" ]\n",
|
||||
" pending_writes = _load_writes(\n",
|
||||
" self.serde,\n",
|
||||
" {\n",
|
||||
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): await self.conn.hgetall(key)\n",
|
||||
" for key, parsed_key in sorted(\n",
|
||||
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
|
||||
" )\n",
|
||||
" },\n",
|
||||
" )\n",
|
||||
" return pending_writes\n",
|
||||
"\n",
|
||||
" async def _aget_checkpoint_key(\n",
|
||||
" self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
|
||||
@@ -1042,7 +1087,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -225,9 +225,19 @@
|
||||
"# Define the function that responds to the user\n",
|
||||
"def respond(state: AgentState):\n",
|
||||
" # Construct the final answer from the arguments of the last tool call\n",
|
||||
" response = WeatherResponse(**state[\"messages\"][-1].tool_calls[0][\"args\"])\n",
|
||||
" weather_tool_call = state[\"messages\"][-1].tool_calls[0]\n",
|
||||
" response = WeatherResponse(**weather_tool_call[\"args\"])\n",
|
||||
" # Since we're using tool calling to return structured output,\n",
|
||||
" # we need to add a tool message corresponding to the WeatherResponse tool call,\n",
|
||||
" # This is due to LLM providers' requirement that AI messages with tool calls\n",
|
||||
" # need to be followed by a tool message for each tool call\n",
|
||||
" tool_message = {\n",
|
||||
" \"type\": \"tool\",\n",
|
||||
" \"content\": \"Here is your structured response\",\n",
|
||||
" \"tool_call_id\": weather_tool_call[\"id\"],\n",
|
||||
" }\n",
|
||||
" # We return the final answer\n",
|
||||
" return {\"final_response\": response}\n",
|
||||
" return {\"final_response\": response, \"messages\": [tool_message]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function that determines whether to continue or not\n",
|
||||
@@ -466,7 +476,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"source": [
|
||||
"# How to call tools using ToolNode\n",
|
||||
"\n",
|
||||
"This guide covers how to use LangGraph's prebuilt [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode) for tool calling.\n",
|
||||
"This guide covers how to use LangGraph's prebuilt [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode) for tool calling.\n",
|
||||
"\n",
|
||||
"`ToolNode` is a LangChain Runnable that takes graph state (with a list of messages) as input and outputs state update with the result of tool calls. It is designed to work well out-of-box with LangGraph's prebuilt [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/), but can also work with any `StateGraph` as long as its state has a `messages` key with an appropriate reducer (see [`MessagesState`](https://github.com/langchain-ai/langgraph/blob/e3ef9adac7395e5c0943c22bbc8a4a856b103aa3/libs/langgraph/langgraph/graph/message.py#L150))."
|
||||
]
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7c58c957-83d8-44ff-8580-a9b3dd39a0a9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to update graph state from tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "95f30587-8dd2-40be-920d-59539089c09f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Command](../../concepts/low_level/#command)\n",
|
||||
"\n",
|
||||
"A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={\"my_custom_key\": \"foo\", \"messages\": [...]})` from the tool:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"@tool\n",
|
||||
"def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):\n",
|
||||
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
|
||||
" user_info = get_user_info(config)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" # update the state keys\n",
|
||||
" \"user_info\": user_info,\n",
|
||||
" # update the message history\n",
|
||||
" \"messages\": [ToolMessage(\"Successfully looked up user information\", tool_call_id=tool_call_id)]\n",
|
||||
" }\n",
|
||||
" )\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"!!! important\n",
|
||||
"\n",
|
||||
" If you want to use tools that return `Command` and update graph state, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:\n",
|
||||
" \n",
|
||||
" ```python\n",
|
||||
" def call_tools(state):\n",
|
||||
" ...\n",
|
||||
" commands = [tools_by_name[tool_call[\"name\"]].invoke(tool_call) for tool_call in tool_calls]\n",
|
||||
" return commands\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"This guide shows how you can do this using LangGraph's prebuilt components ([`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode]).\n",
|
||||
"\n",
|
||||
"!!! note\n",
|
||||
"\n",
|
||||
" Support for tools that return [`Command`][langgraph.types.Command] was added in LangGraph `v0.2.59`.\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "64500eca-1cdc-43d9-9401-f4cd9999881f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "a3f92fb2-9175-47fa-9c7d-ad5f44bfd20e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Please provide your OPENAI_API_KEY ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import getpass\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_if_undefined(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_if_undefined(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "caf6ff9f-c1e6-499e-a230-9fa231ea7d2f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "10e9a9c6-fa3f-416c-bac0-3e58d7259908",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's create a simple ReAct style agent that can look up user information and personalize the response based on the user info."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4255b9b9-cf67-4cc3-8018-1708f5dfcfd2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define tool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7de6b010-aab1-4fe8-8251-907fcae78583",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"First, let's define the tool that we'll be using to look up user information. We'll use a naive implementation that simply looks user information up using a dictionary:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8d070c9f-6e61-4724-85dc-ac4531b9c79a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"USER_INFO = [\n",
|
||||
" {\"user_id\": \"1\", \"name\": \"Bob Dylan\", \"location\": \"New York, NY\"},\n",
|
||||
" {\"user_id\": \"2\", \"name\": \"Taylor Swift\", \"location\": \"Beverly Hills, CA\"},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"USER_ID_TO_USER_INFO = {info[\"user_id\"]: info for info in USER_INFO}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "08d1ecca-ee57-4e97-b8d0-e09de85337d4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.prebuilt.chat_agent_executor import AgentState\n",
|
||||
"from langgraph.types import Command\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"from langchain_core.tools.base import InjectedToolCallId\n",
|
||||
"from langchain_core.messages import ToolMessage\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"\n",
|
||||
"from typing_extensions import Any, Annotated\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(AgentState):\n",
|
||||
" # updated by the tool\n",
|
||||
" user_info: dict[str, Any]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def lookup_user_info(\n",
|
||||
" tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig\n",
|
||||
"):\n",
|
||||
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
|
||||
" user_id = config.get(\"configurable\", {}).get(\"user_id\")\n",
|
||||
" if user_id is None:\n",
|
||||
" raise ValueError(\"Please provide user ID\")\n",
|
||||
"\n",
|
||||
" if user_id not in USER_ID_TO_USER_INFO:\n",
|
||||
" raise ValueError(f\"User '{user_id}' not found\")\n",
|
||||
"\n",
|
||||
" user_info = USER_ID_TO_USER_INFO[user_id]\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" # update the state keys\n",
|
||||
" \"user_info\": user_info,\n",
|
||||
" # update the message history\n",
|
||||
" \"messages\": [\n",
|
||||
" ToolMessage(\n",
|
||||
" \"Successfully looked up user information\", tool_call_id=tool_call_id\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
" }\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b99e5f24-5e5e-4a34-baae-467182675bb5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define prompt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cbb06aea-6654-4245-91f8-af6e8f2b5377",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now add personalization: we'll respond differently to the user based on the state values AFTER the state has been updated from the tool. To achieve this, let's define a function that will dynamically construct the system prompt based on the graph state. It will be called ever time the LLM is called and the function output will be passed to the LLM:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "c553d062-d145-4145-84bd-9b798f7c95c2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def state_modifier(state: State):\n",
|
||||
" user_info = state.get(\"user_info\")\n",
|
||||
" if user_info is None:\n",
|
||||
" return state[\"messages\"]\n",
|
||||
"\n",
|
||||
" system_msg = (\n",
|
||||
" f\"User name is {user_info['name']}. User lives in {user_info['location']}\"\n",
|
||||
" )\n",
|
||||
" return [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c5acdd5d-68be-466b-9c21-46cbed91d2bc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define graph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "afb65028-0359-46c8-b09c-ffc90180f759",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Finally, let's combine this into a single graph using the prebuilt `create_react_agent`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "2d59db29-fd51-4d29-9854-21763a4855e3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(model=\"gpt-4o\")\n",
|
||||
"\n",
|
||||
"agent = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" # pass the tool that can update state\n",
|
||||
" [lookup_user_info],\n",
|
||||
" state_schema=State,\n",
|
||||
" # pass dynamic prompt function\n",
|
||||
" state_modifier=state_modifier,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0782b8ab-a603-47b8-9a76-77f593402678",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use it!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6165e153-ab28-4404-adea-796c7bd0701b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now try running our agent. We'll need to provide user ID in the config so that our tool knows what information to look up:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "de34a58b-1765-4b63-a232-d46790aff884",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-57eeb216-e35d-4501-aaac-b5c6b26fb17c-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"{'tools': {'user_info': {'user_id': '1', 'name': 'Bob Dylan', 'location': 'New York, NY'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='168d8ff8-b021-4c8b-a11a-3b50c30a072c', tool_call_id='call_7LSUh6ZDvGJAUvlWvXiCK4Gf')]}}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"{'agent': {'messages': [AIMessage(content=\"Hi Bob! Since you're in New York, NY, there are plenty of exciting things to do over the weekend. Here are some suggestions:\\n\\n1. **Explore Central Park**: Take a leisurely walk, rent a bike, or have a picnic in this iconic park.\\n\\n2. **Visit a Museum**: Check out The Metropolitan Museum of Art or the Museum of Modern Art (MoMA) for an enriching cultural experience.\\n\\n3. **Broadway Show**: Catch a Broadway show or an off-Broadway performance for some world-class entertainment.\\n\\n4. **Food Tour**: Explore different neighborhoods like Greenwich Village or Williamsburg for diverse culinary experiences.\\n\\n5. **Brooklyn Bridge Walk**: Take a walk across the Brooklyn Bridge for stunning views of the city skyline.\\n\\n6. **Visit a Rooftop Bar**: Enjoy a drink with a view at one of New York’s many rooftop bars.\\n\\n7. **Explore a New Neighborhood**: Discover the unique charm of areas like SoHo, Chelsea, or Astoria.\\n\\n8. **Live Music**: Check out live music venues for a night of great performances.\\n\\n9. **Art Galleries**: Visit some of the smaller art galleries around Chelsea or the Lower East Side.\\n\\n10. **Attend a Local Event**: Look up any local events or festivals happening this weekend.\\n\\nFeel free to let me know if you want more details on any of these activities!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 285, 'prompt_tokens': 95, 'total_tokens': 380, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'stop', 'logprobs': None}, id='run-f13ce15b-02b6-40e6-8264-c4d9edd0d03a-0', usage_metadata={'input_tokens': 95, 'output_tokens': 285, 'total_tokens': 380, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in agent.stream(\n",
|
||||
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
|
||||
" # provide user ID in the config\n",
|
||||
" {\"configurable\": {\"user_id\": \"1\"}},\n",
|
||||
"):\n",
|
||||
" print(chunk)\n",
|
||||
" print(\"\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d9b2281f-269c-41dd-b6b2-4c743f11ffc9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can see that the model correctly recommended some New York activities for Bob Dylan! Let's try getting recommendations for Taylor Swift:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "9d71af94-572a-4961-88a7-665e792cf96a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-bacacd7d-76cc-4f6b-9e9b-d9e6f00b9391-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"{'tools': {'user_info': {'user_id': '2', 'name': 'Taylor Swift', 'location': 'Beverly Hills, CA'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='d81ef31e-6d77-4f13-ae86-e2e6ba567e3d', tool_call_id='call_5HLtJtzcgmKbtmK6By21wW5Y')]}}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"{'agent': {'messages': [AIMessage(content=\"Hi Taylor! Since you're in Beverly Hills, here are a few suggestions for a fun weekend:\\n\\n1. **Hiking at Runyon Canyon**: Enjoy a scenic hike with beautiful views of Los Angeles. It's a great way to get some exercise and enjoy the outdoors.\\n\\n2. **Visit Rodeo Drive**: Spend some time shopping or window shopping at the famous Rodeo Drive. You might even spot some celebrities!\\n\\n3. **Explore the Getty Center**: Check out the art collections and beautiful gardens at the Getty Center. The architecture and views are stunning.\\n\\n4. **Relax at a Spa**: Treat yourself to a relaxing day at one of Beverly Hills' luxurious spas.\\n\\n5. **Dining Out**: Try a new restaurant or visit your favorite spot for a delicious meal. Beverly Hills has a fantastic dining scene.\\n\\n6. **Attend a Local Event**: Check out any local events or concerts happening this weekend. Beverly Hills often hosts exciting events.\\n\\nEnjoy your weekend!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 198, 'prompt_tokens': 95, 'total_tokens': 293, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'stop', 'logprobs': None}, id='run-2057df76-f192-4c69-a66a-1f0a86bf5d66-0', usage_metadata={'input_tokens': 95, 'output_tokens': 198, 'total_tokens': 293, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in agent.stream(\n",
|
||||
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
|
||||
" {\"configurable\": {\"user_id\": \"2\"}},\n",
|
||||
"):\n",
|
||||
" print(chunk)\n",
|
||||
" print(\"\\n\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -11,8 +11,9 @@ 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 Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
|
||||
- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
|
||||
- [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.
|
||||
|
||||
## Use cases 🛠️
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Quick Start: Launch Local LangGraph Server
|
||||
# QuickStart: Launch Local LangGraph Server
|
||||
|
||||
This is a quick start guide to help you get a LangGraph app up and running locally.
|
||||
|
||||
@@ -10,7 +10,7 @@ This is a quick start guide to help you get a LangGraph app up and running local
|
||||
## Install the LangGraph CLI
|
||||
|
||||
```bash
|
||||
pip install "langgraph-cli[inmem]==0.1.58" python-dotenv
|
||||
pip install --upgrade "langgraph-cli[inmem]"
|
||||
```
|
||||
|
||||
## 🌱 Create a LangGraph App
|
||||
@@ -53,21 +53,12 @@ ANTHROPIC_API_KEY=sk-
|
||||
OPENAI_API_KEY=sk-...
|
||||
```
|
||||
|
||||
<details><summary>Get API Keys</summary>
|
||||
<ul>
|
||||
<li> <b>LANGSMITH_API_KEY</b>: Go to the <a href="https://smith.langchain.com/settings">LangSmith Settings page</a>. Then clck <b>Create API Key</b>.
|
||||
</li>
|
||||
<li>
|
||||
<b>ANTHROPIC_API_KEY</b>: Get an API key from <a href="https://console.anthropic.com/">Anthropic</a>.
|
||||
</li>
|
||||
<li>
|
||||
<b>OPENAI_API_KEY</b>: Get an API key from <a href="https://openai.com/">OpenAI</a>.
|
||||
</li>
|
||||
<li>
|
||||
<b>TAVILY_API_KEY</b>: Get an API key on the <a href="https://app.tavily.com/">Tavily website</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
??? note "Get API Keys"
|
||||
|
||||
- **LANGSMITH_API_KEY**: Go to the [LangSmith Settings page](https://smith.langchain.com/settings). Then clck **Create API Key**.
|
||||
- **ANTHROPIC_API_KEY**: Get an API key from [Anthropic](https://console.anthropic.com/).
|
||||
- **OPENAI_API_KEY**: Get an API key from [OpenAI](https://openai.com/).
|
||||
- **TAVILY_API_KEY**: Get an API key on the [Tavily website](https://app.tavily.com/).
|
||||
|
||||
## 🚀 Launch LangGraph Server
|
||||
|
||||
@@ -79,11 +70,11 @@ This will start up the LangGraph API server locally. If this runs successfully,
|
||||
|
||||
> Ready!
|
||||
>
|
||||
> - API: [http://localhost:8123](http://localhost:8123/)
|
||||
> - API: [http://localhost:2024](http://localhost:2024/)
|
||||
>
|
||||
> - Docs: http://localhost:8123/docs
|
||||
> - Docs: http://localhost:2024/docs
|
||||
>
|
||||
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
|
||||
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
||||
|
||||
|
||||
!!! note "In-Memory Mode"
|
||||
@@ -95,9 +86,9 @@ This will start up the LangGraph API server locally. If this runs successfully,
|
||||
|
||||
## LangGraph Studio Web UI
|
||||
|
||||
Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph up` command.
|
||||
Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph dev` command.
|
||||
|
||||
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
|
||||
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
||||
|
||||
!!! warning "Safari Compatibility"
|
||||
|
||||
@@ -118,7 +109,7 @@ Test your graph in the LangGraph Studio Web UI by visiting the URL provided in t
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="http://localhost:8123")
|
||||
client = get_client(url="http://localhost:2024")
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
None, # Threadless run
|
||||
@@ -149,7 +140,7 @@ Test your graph in the LangGraph Studio Web UI by visiting the URL provided in t
|
||||
```python
|
||||
from langgraph_sdk import get_sync_client
|
||||
|
||||
client = get_sync_client(url="http://localhost:8123")
|
||||
client = get_sync_client(url="http://localhost:2024")
|
||||
|
||||
for chunk in client.runs.stream(
|
||||
None, # Threadless run
|
||||
@@ -181,7 +172,7 @@ Test your graph in the LangGraph Studio Web UI by visiting the URL provided in t
|
||||
const { Client } = await import("@langchain/langgraph-sdk");
|
||||
|
||||
// only set the apiUrl if you changed the default port when calling langgraph up
|
||||
const client = new Client({ apiUrl: "http://localhost:8123"});
|
||||
const client = new Client({ apiUrl: "http://localhost:2024"});
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
null, // Threadless run
|
||||
@@ -207,7 +198,7 @@ Test your graph in the LangGraph Studio Web UI by visiting the URL provided in t
|
||||
|
||||
```bash
|
||||
curl -s --request POST \
|
||||
--url "http://localhost:8123/runs/stream" \
|
||||
--url "http://localhost:2024/runs/stream" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
@@ -235,7 +226,7 @@ Now that you have a LangGraph app running locally, take your journey further by
|
||||
|
||||
### 🌐 Deploy to LangGraph Cloud
|
||||
|
||||
- **[LangGraph Cloud QuickStart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
|
||||
- **[LangGraph Cloud Quickstart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
|
||||
|
||||
### 📚 Learn More about LangGraph Platform
|
||||
|
||||
@@ -250,4 +241,4 @@ Access detailed documentation for development and API usage:
|
||||
|
||||
- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
|
||||
- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
|
||||
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
|
||||
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
|
||||
|
||||
@@ -289,15 +289,10 @@
|
||||
"from langchain_core.language_models.chat_models import BaseChatModel\n",
|
||||
"\n",
|
||||
"from langgraph.graph import StateGraph, MessagesState, START, END\n",
|
||||
"from langgraph.types import Command\n",
|
||||
"from langchain_core.messages import HumanMessage, trim_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# The agent state is the input to each node in the graph\n",
|
||||
"class AgentState(MessagesState):\n",
|
||||
" # The 'next' field indicates where to route to next\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def make_supervisor_node(llm: BaseChatModel, members: list[str]) -> str:\n",
|
||||
" options = [\"FINISH\"] + members\n",
|
||||
" system_prompt = (\n",
|
||||
@@ -313,17 +308,17 @@
|
||||
"\n",
|
||||
" next: Literal[*options]\n",
|
||||
"\n",
|
||||
" def supervisor_node(state: MessagesState) -> MessagesState:\n",
|
||||
" def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
|
||||
" \"\"\"An LLM-based router.\"\"\"\n",
|
||||
" messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": system_prompt},\n",
|
||||
" ] + state[\"messages\"]\n",
|
||||
" response = llm.with_structured_output(Router).invoke(messages)\n",
|
||||
" next_ = response[\"next\"]\n",
|
||||
" if next_ == \"FINISH\":\n",
|
||||
" next_ = END\n",
|
||||
" goto = response[\"next\"]\n",
|
||||
" if goto == \"FINISH\":\n",
|
||||
" goto = END\n",
|
||||
"\n",
|
||||
" return {\"next\": next_}\n",
|
||||
" return Command(goto=goto)\n",
|
||||
"\n",
|
||||
" return supervisor_node"
|
||||
]
|
||||
@@ -363,25 +358,33 @@
|
||||
"search_agent = create_react_agent(llm, tools=[tavily_tool])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def search_node(state: AgentState) -> AgentState:\n",
|
||||
"def search_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = search_agent.invoke(state)\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"search\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"search\")\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"web_scraper_agent = create_react_agent(llm, tools=[scrape_webpages])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def web_scraper_node(state: AgentState) -> AgentState:\n",
|
||||
"def web_scraper_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = web_scraper_agent.invoke(state)\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"web_scraper\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"web_scraper\")\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_supervisor_node = make_supervisor_node(llm, [\"search\", \"web_scraper\"])"
|
||||
@@ -412,14 +415,7 @@
|
||||
"research_builder.add_node(\"search\", search_node)\n",
|
||||
"research_builder.add_node(\"web_scraper\", web_scraper_node)\n",
|
||||
"\n",
|
||||
"# Define the control flow\n",
|
||||
"research_builder.add_edge(START, \"supervisor\")\n",
|
||||
"# We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
"research_builder.add_edge(\"search\", \"supervisor\")\n",
|
||||
"research_builder.add_edge(\"web_scraper\", \"supervisor\")\n",
|
||||
"# Add the edges where routing applies\n",
|
||||
"research_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
|
||||
"\n",
|
||||
"research_graph = research_builder.compile()"
|
||||
]
|
||||
},
|
||||
@@ -532,13 +528,17 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def doc_writing_node(state: AgentState) -> AgentState:\n",
|
||||
"def doc_writing_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = doc_writer_agent.invoke(state)\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"doc_writer\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"doc_writer\")\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"note_taking_agent = create_react_agent(\n",
|
||||
@@ -551,13 +551,17 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def note_taking_node(state: AgentState) -> AgentState:\n",
|
||||
"def note_taking_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = note_taking_agent.invoke(state)\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"note_taker\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"note_taker\")\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"chart_generating_agent = create_react_agent(\n",
|
||||
@@ -565,13 +569,19 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def chart_generating_node(state: AgentState) -> AgentState:\n",
|
||||
"def chart_generating_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = chart_generating_agent.invoke(state)\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=result[\"messages\"][-1].content, name=\"chart_generator\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=result[\"messages\"][-1].content, name=\"chart_generator\"\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"doc_writing_supervisor_node = make_supervisor_node(\n",
|
||||
@@ -600,21 +610,13 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create the graph here\n",
|
||||
"paper_writing_builder = StateGraph(AgentState)\n",
|
||||
"paper_writing_builder = StateGraph(MessagesState)\n",
|
||||
"paper_writing_builder.add_node(\"supervisor\", doc_writing_supervisor_node)\n",
|
||||
"paper_writing_builder.add_node(\"doc_writer\", doc_writing_node)\n",
|
||||
"paper_writing_builder.add_node(\"note_taker\", note_taking_node)\n",
|
||||
"paper_writing_builder.add_node(\"chart_generator\", chart_generating_node)\n",
|
||||
"\n",
|
||||
"# Define the control flow\n",
|
||||
"paper_writing_builder.add_edge(START, \"supervisor\")\n",
|
||||
"# We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
|
||||
"paper_writing_builder.add_edge(\"doc_writer\", \"supervisor\")\n",
|
||||
"paper_writing_builder.add_edge(\"note_taker\", \"supervisor\")\n",
|
||||
"paper_writing_builder.add_edge(\"chart_generator\", \"supervisor\")\n",
|
||||
"# Add the edges where routing applies\n",
|
||||
"paper_writing_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
|
||||
"\n",
|
||||
"paper_writing_graph = paper_writing_builder.compile()"
|
||||
]
|
||||
},
|
||||
@@ -728,37 +730,41 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def call_research_team(state: AgentState) -> AgentState:\n",
|
||||
"def call_research_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" response = research_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=response[\"messages\"][-1].content, name=\"research_team\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=response[\"messages\"][-1].content, name=\"research_team\"\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_paper_writing_team(state: AgentState) -> AgentState:\n",
|
||||
"def call_paper_writing_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" response = paper_writing_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
|
||||
" return {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=response[\"messages\"][-1].content, name=\"writing_team\")\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=response[\"messages\"][-1].content, name=\"writing_team\"\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
" goto=\"supervisor\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the graph.\n",
|
||||
"super_builder = StateGraph(AgentState)\n",
|
||||
"super_builder = StateGraph(MessagesState)\n",
|
||||
"super_builder.add_node(\"supervisor\", teams_supervisor_node)\n",
|
||||
"super_builder.add_node(\"research_team\", call_research_team)\n",
|
||||
"super_builder.add_node(\"writing_team\", call_paper_writing_team)\n",
|
||||
"\n",
|
||||
"# Define the control flow\n",
|
||||
"super_builder.add_edge(START, \"supervisor\")\n",
|
||||
"# We want our teams to ALWAYS \"report back\" to the top-level supervisor when done\n",
|
||||
"super_builder.add_edge(\"research_team\", \"supervisor\")\n",
|
||||
"super_builder.add_edge(\"writing_team\", \"supervisor\")\n",
|
||||
"# Add the edges where routing applies\n",
|
||||
"super_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
|
||||
"super_graph = super_builder.compile()"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -192,6 +192,7 @@ nav:
|
||||
- how-tos/tool-calling.ipynb
|
||||
- how-tos/tool-calling-errors.ipynb
|
||||
- how-tos/pass-run-time-values-to-tools.ipynb
|
||||
- how-tos/update-state-from-tools.ipynb
|
||||
- how-tos/pass-config-to-tools.ipynb
|
||||
- how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
@@ -199,6 +200,8 @@ nav:
|
||||
- how-tos/subgraph.ipynb
|
||||
- how-tos/subgraphs-manage-state.ipynb
|
||||
- how-tos/subgraph-transform-state.ipynb
|
||||
- Multi-agent:
|
||||
- how-tos/multi-agent-network.ipynb
|
||||
- State Management:
|
||||
- State Management: how-tos#state-management
|
||||
- how-tos/state-model.ipynb
|
||||
@@ -281,6 +284,7 @@ nav:
|
||||
- cloud/how-tos/test_local_deployment.md
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- Troubleshooting:
|
||||
- Troubleshooting: how-tos#troubleshooting
|
||||
- troubleshooting/errors/index.md
|
||||
|
||||
@@ -57,6 +57,17 @@ MIGRATIONS = [
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
||||
);""",
|
||||
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
||||
"""
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id);
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
||||
""",
|
||||
]
|
||||
|
||||
SELECT_SQL = f"""
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, Callable, Optional, Union, cast
|
||||
|
||||
import orjson
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
@@ -156,9 +155,6 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
|
||||
return results
|
||||
|
||||
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def from_conn_string(
|
||||
@@ -219,22 +215,19 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
"""
|
||||
|
||||
async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
|
||||
try:
|
||||
await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
||||
row = await cur.fetchone()
|
||||
if row is None:
|
||||
version = -1
|
||||
else:
|
||||
version = row["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
await cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table} (
|
||||
v INTEGER PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
await cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table} (
|
||||
v INTEGER PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
)
|
||||
await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
||||
row = cast(dict, await cur.fetchone())
|
||||
if row is None:
|
||||
version = -1
|
||||
else:
|
||||
version = row["v"]
|
||||
return version
|
||||
|
||||
async with self._cursor() as cur:
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing import (
|
||||
|
||||
import orjson
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
@@ -73,7 +72,7 @@ CREATE TABLE IF NOT EXISTS store (
|
||||
""",
|
||||
"""
|
||||
-- For faster lookups by prefix
|
||||
CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
||||
""",
|
||||
]
|
||||
|
||||
@@ -107,7 +106,7 @@ CREATE TABLE IF NOT EXISTS store_vectors (
|
||||
),
|
||||
Migration(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS store_vectors_embedding_idx ON store_vectors
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS store_vectors_embedding_idx ON store_vectors
|
||||
USING %(index_type)s (embedding %(ops)s)%(index_params)s;
|
||||
""",
|
||||
condition=lambda store: bool(
|
||||
@@ -573,6 +572,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
|
||||
# Search by similarity
|
||||
results = store.search(("docs",), query="python programming")
|
||||
```
|
||||
|
||||
Note:
|
||||
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
||||
@@ -846,22 +846,19 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
"""
|
||||
|
||||
def _get_version(cur: Cursor[dict[str, Any]], table: str) -> int:
|
||||
try:
|
||||
cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
||||
row = cast(dict, cur.fetchone())
|
||||
if row is None:
|
||||
version = -1
|
||||
else:
|
||||
version = row["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table} (
|
||||
v INTEGER PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table} (
|
||||
v INTEGER PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
||||
row = cast(dict, cur.fetchone())
|
||||
if row is None:
|
||||
version = -1
|
||||
else:
|
||||
version = row["v"]
|
||||
return version
|
||||
|
||||
with self._cursor() as cur:
|
||||
|
||||
@@ -13,24 +13,24 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.6.2.post1"
|
||||
version = "4.7.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
|
||||
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
|
||||
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
|
||||
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -244,13 +244,13 @@ trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
version = "0.28.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
{file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"},
|
||||
{file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -258,7 +258,6 @@ anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
@@ -342,7 +341,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.7"
|
||||
version = "2.0.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -741,13 +740,13 @@ typing-extensions = ">=4.6"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.2"
|
||||
version = "2.10.3"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
|
||||
{file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
|
||||
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
|
||||
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.7"
|
||||
version = "2.0.8"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -63,9 +63,8 @@ async def _pipe_saver():
|
||||
prepare_threshold=0,
|
||||
row_factory=dict_row,
|
||||
) as conn:
|
||||
async with conn.pipeline() as pipe:
|
||||
checkpointer = AsyncPostgresSaver(conn, pipe=pipe)
|
||||
await checkpointer.setup()
|
||||
checkpointer = AsyncPostgresSaver(conn)
|
||||
await checkpointer.setup()
|
||||
async with conn.pipeline() as pipe:
|
||||
checkpointer = AsyncPostgresSaver(conn, pipe=pipe)
|
||||
yield checkpointer
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# type: ignore
|
||||
import asyncio
|
||||
import itertools
|
||||
import sys
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -10,7 +12,13 @@ import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
ListNamespacesOp,
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
from langgraph.store.postgres import AsyncPostgresStore
|
||||
from tests.conftest import (
|
||||
DEFAULT_URI,
|
||||
@@ -63,6 +71,128 @@ async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
async def test_no_running_loop(store: AsyncPostgresStore) -> None:
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.put(("foo", "bar"), "baz", {"val": "baz"})
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.get(("foo", "bar"), "baz")
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.delete(("foo", "bar"), "baz")
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.search(("foo", "bar"))
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.list_namespaces(prefix=("foo",))
|
||||
with pytest.raises(asyncio.InvalidStateError):
|
||||
store.batch([PutOp(namespace=("foo", "bar"), key="baz", value={"val": "baz"})])
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(store.put, ("foo", "bar"), "baz", {"val": "baz"})
|
||||
result = await asyncio.wrap_future(future)
|
||||
assert result is None
|
||||
future = executor.submit(store.get, ("foo", "bar"), "baz")
|
||||
result = await asyncio.wrap_future(future)
|
||||
assert result.value == {"val": "baz"}
|
||||
result = await asyncio.wrap_future(
|
||||
executor.submit(store.list_namespaces, prefix=("foo",))
|
||||
)
|
||||
|
||||
|
||||
async def test_large_batches(request: Any, store: AsyncPostgresStore) -> None:
|
||||
N = 100 # less important that we are performant here
|
||||
M = 10
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = []
|
||||
for m in range(M):
|
||||
for i in range(N):
|
||||
futures += [
|
||||
executor.submit(
|
||||
store.put,
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
value={"foo": "bar" + str(i)},
|
||||
),
|
||||
executor.submit(
|
||||
store.get,
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
),
|
||||
executor.submit(
|
||||
store.list_namespaces,
|
||||
prefix=None,
|
||||
max_depth=m + 1,
|
||||
),
|
||||
executor.submit(
|
||||
store.search,
|
||||
("test",),
|
||||
),
|
||||
executor.submit(
|
||||
store.put,
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
value={"foo": "bar" + str(i)},
|
||||
),
|
||||
executor.submit(
|
||||
store.put,
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
None,
|
||||
),
|
||||
]
|
||||
|
||||
results = await asyncio.gather(
|
||||
*(asyncio.wrap_future(future) for future in futures)
|
||||
)
|
||||
assert len(results) == M * N * 6
|
||||
|
||||
|
||||
async def test_large_batches_async(store: AsyncPostgresStore) -> None:
|
||||
N = 1000
|
||||
M = 10
|
||||
coros = []
|
||||
for m in range(M):
|
||||
for i in range(N):
|
||||
coros.append(
|
||||
store.aput(
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
value={"foo": "bar" + str(i)},
|
||||
)
|
||||
)
|
||||
coros.append(
|
||||
store.aget(
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
)
|
||||
)
|
||||
coros.append(
|
||||
store.alist_namespaces(
|
||||
prefix=None,
|
||||
max_depth=m + 1,
|
||||
)
|
||||
)
|
||||
coros.append(
|
||||
store.asearch(
|
||||
("test",),
|
||||
)
|
||||
)
|
||||
coros.append(
|
||||
store.aput(
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
value={"foo": "bar" + str(i)},
|
||||
)
|
||||
)
|
||||
coros.append(
|
||||
store.adelete(
|
||||
("test", "foo", "bar", "baz", str(m % 2)),
|
||||
f"key{i}",
|
||||
)
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*coros)
|
||||
assert len(results) == M * N * 6
|
||||
|
||||
|
||||
async def test_abatch_order(store: AsyncPostgresStore) -> None:
|
||||
# Setup test data
|
||||
await store.aput(("test", "foo"), "key1", {"data": "value1"})
|
||||
|
||||
@@ -57,9 +57,8 @@ def _pipe_saver():
|
||||
prepare_threshold=0,
|
||||
row_factory=dict_row,
|
||||
) as conn:
|
||||
with conn.pipeline() as pipe:
|
||||
checkpointer = PostgresSaver(conn, pipe=pipe)
|
||||
checkpointer.setup()
|
||||
checkpointer = PostgresSaver(conn)
|
||||
checkpointer.setup()
|
||||
with conn.pipeline() as pipe:
|
||||
checkpointer = PostgresSaver(conn, pipe=pipe)
|
||||
yield checkpointer
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
@@ -6,7 +5,6 @@ import random
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
||||
from functools import partial
|
||||
from types import TracebackType
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
|
||||
|
||||
@@ -395,9 +393,7 @@ class MemorySaver(
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, self.get_tuple, config
|
||||
)
|
||||
return self.get_tuple(config)
|
||||
|
||||
async def alist(
|
||||
self,
|
||||
@@ -418,24 +414,8 @@ class MemorySaver(
|
||||
Yields:
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
iter = await loop.run_in_executor(
|
||||
None,
|
||||
partial(
|
||||
self.list,
|
||||
before=before,
|
||||
limit=limit,
|
||||
filter=filter,
|
||||
),
|
||||
config,
|
||||
)
|
||||
while True:
|
||||
# handling StopIteration exception inside coroutine won't work
|
||||
# as expected, so using next() with default value to break the loop
|
||||
if item := await loop.run_in_executor(None, next, iter, None):
|
||||
yield item
|
||||
else:
|
||||
break
|
||||
for item in self.list(config, filter=filter, before=before, limit=limit):
|
||||
yield item
|
||||
|
||||
async def aput(
|
||||
self,
|
||||
@@ -455,9 +435,7 @@ class MemorySaver(
|
||||
Returns:
|
||||
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
|
||||
"""
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, self.put, config, checkpoint, metadata, new_versions
|
||||
)
|
||||
return self.put(config, checkpoint, metadata, new_versions)
|
||||
|
||||
async def aput_writes(
|
||||
self,
|
||||
@@ -474,10 +452,9 @@ class MemorySaver(
|
||||
config (RunnableConfig): The config to associate with the writes.
|
||||
writes (List[Tuple[str, Any]]): The writes to save, each as a (channel, value) pair.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
return self.put_writes(config, writes, task_id)
|
||||
"""
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, self.put_writes, config, writes, task_id
|
||||
)
|
||||
return self.put_writes(config, writes, task_id)
|
||||
|
||||
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
||||
if current is None:
|
||||
|
||||
@@ -438,28 +438,36 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, arg
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, arg, method
|
||||
return getattr(getattr(importlib.import_module(tup[0]), tup[1]), tup[3])(
|
||||
tup[2]
|
||||
@@ -468,7 +476,9 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, kwargs
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
@@ -479,7 +489,9 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
)
|
||||
# module, name, kwargs, method
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
|
||||
@@ -80,13 +80,16 @@ class Item:
|
||||
|
||||
def dict(self) -> dict:
|
||||
return {
|
||||
"value": self.value,
|
||||
"key": self.key,
|
||||
"namespace": list(self.namespace),
|
||||
"key": self.key,
|
||||
"value": self.value,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Item({', '.join(f'{k}={v!r}' for k, v in self.dict().items())})"
|
||||
|
||||
|
||||
class SearchItem(Item):
|
||||
"""Represents an item returned from a search operation with additional metadata."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import weakref
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
|
||||
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
@@ -11,11 +12,39 @@ from langgraph.store.base import (
|
||||
NamespacePath,
|
||||
Op,
|
||||
PutOp,
|
||||
Result,
|
||||
SearchItem,
|
||||
SearchOp,
|
||||
_validate_namespace,
|
||||
)
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
|
||||
def _check_loop(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
def wrapper(store: "AsyncBatchedBaseStore", *args: Any, **kwargs: Any) -> Any:
|
||||
method_name: str = func.__name__
|
||||
try:
|
||||
current_loop = asyncio.get_running_loop()
|
||||
if current_loop is store._loop:
|
||||
replacement_str = (
|
||||
f"Specifically, replace `store.{method_name}(...)` with `await store.a{method_name}(...)"
|
||||
if method_name
|
||||
else "For example, replace `store.get(...)` with `await store.aget(...)`"
|
||||
)
|
||||
raise asyncio.InvalidStateError(
|
||||
f"Synchronous calls to {store.__class__.__name__} detected in the main event loop. "
|
||||
"This can lead to deadlocks or performance issues. "
|
||||
"Please use the asynchronous interface for main thread operations. "
|
||||
f"{replacement_str} "
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
return func(store, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class AsyncBatchedBaseStore(BaseStore):
|
||||
"""Efficiently batch operations in a background task."""
|
||||
@@ -23,6 +52,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
__slots__ = ("_loop", "_aqueue", "_task")
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._aqueue: dict[asyncio.Future, Op] = {}
|
||||
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
|
||||
@@ -99,6 +129,82 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
self._aqueue[fut] = op
|
||||
return await fut
|
||||
|
||||
@_check_loop
|
||||
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self._loop).result()
|
||||
|
||||
@_check_loop
|
||||
def get(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> Optional[Item]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget(namespace, key=key), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
def search(
|
||||
self,
|
||||
namespace_prefix: tuple[str, ...],
|
||||
/,
|
||||
*,
|
||||
query: Optional[str] = None,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.asearch(
|
||||
namespace_prefix, query=query, filter=filter, limit=limit, offset=offset
|
||||
),
|
||||
self._loop,
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
def put(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
) -> None:
|
||||
_validate_namespace(namespace)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.aput(namespace, key=key, value=value, index=index), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
def delete(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> None:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.adelete(namespace, key=key), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
def list_namespaces(
|
||||
self,
|
||||
*,
|
||||
prefix: Optional[NamespacePath] = None,
|
||||
suffix: Optional[NamespacePath] = None,
|
||||
max_depth: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.alist_namespaces(
|
||||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
max_depth=max_depth,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
),
|
||||
self._loop,
|
||||
).result()
|
||||
|
||||
|
||||
def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
|
||||
"""Dedupe operations while preserving order for results.
|
||||
@@ -144,7 +250,8 @@ def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
|
||||
|
||||
|
||||
async def _run(
|
||||
aqueue: dict[asyncio.Future, Op], store: weakref.ReferenceType[BaseStore]
|
||||
aqueue: dict[asyncio.Future, Op],
|
||||
store: weakref.ReferenceType[BaseStore],
|
||||
) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.8"
|
||||
version = "2.0.9"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -576,16 +576,18 @@ def dev(
|
||||
from langgraph_api.cli import run_server
|
||||
except ImportError:
|
||||
try:
|
||||
import pkg_resources
|
||||
from importlib import util
|
||||
|
||||
pkg_resources.require("langgraph-api-inmem")
|
||||
except (ImportError, pkg_resources.DistributionNotFound):
|
||||
if not util.find_spec("langgraph_api"):
|
||||
raise click.UsageError(
|
||||
"Required package 'langgraph-api' is not installed.\n"
|
||||
"Please install it with:\n\n"
|
||||
' pip install -U "langgraph-cli[inmem]"\n\n'
|
||||
) from None
|
||||
except ImportError:
|
||||
raise click.UsageError(
|
||||
"Required package 'langgraph-api-inmem' is not installed.\n"
|
||||
"Please install it with:\n\n"
|
||||
' pip install -U "langgraph-cli[inmem]"\n\n'
|
||||
"If you're developing the langgraph-cli package locally, you can install in development mode:\n"
|
||||
" pip install -e ."
|
||||
"Could not verify package installation. Please ensure Python is up to date and\n"
|
||||
"langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
|
||||
) from None
|
||||
raise click.UsageError(
|
||||
"Could not import run_server. This likely means your installation is incomplete.\n"
|
||||
@@ -614,6 +616,7 @@ def dev(
|
||||
env=config_json.get("env"),
|
||||
store=config_json.get("store"),
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,44 @@ class StoreConfig(TypedDict, total=False):
|
||||
"""Configuration for vector embeddings in store."""
|
||||
|
||||
|
||||
class SecurityConfig(TypedDict, total=False):
|
||||
securitySchemes: dict
|
||||
security: list
|
||||
# path => {method => security}
|
||||
paths: dict[str, dict[str, list]]
|
||||
|
||||
|
||||
class AuthConfig(TypedDict, total=False):
|
||||
path: str
|
||||
"""Path to the authentication function in a Python file."""
|
||||
disable_studio_auth: bool
|
||||
"""Whether to disable auth when connecting from the LangSmith Studio."""
|
||||
openapi: SecurityConfig
|
||||
"""The schema to use for updating the openapi spec.
|
||||
|
||||
Example:
|
||||
{
|
||||
"securitySchemes": {
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {
|
||||
"me": "Read information about the current user",
|
||||
"items": "Access to create and manage items"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{"OAuth2": ["me"]} # Default security requirement for all endpoints
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
python_version: str
|
||||
node_version: Optional[str]
|
||||
@@ -56,6 +94,7 @@ class Config(TypedDict, total=False):
|
||||
graphs: dict[str, str]
|
||||
env: Union[dict[str, str], str]
|
||||
store: Optional[StoreConfig]
|
||||
auth: Optional[AuthConfig]
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
@@ -85,9 +124,11 @@ def validate_config(config: Config) -> Config:
|
||||
{
|
||||
"node_version": config.get("node_version"),
|
||||
"dockerfile_lines": config.get("dockerfile_lines", []),
|
||||
"dependencies": config.get("dependencies", []),
|
||||
"graphs": config.get("graphs", {}),
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -98,6 +139,7 @@ def validate_config(config: Config) -> Config:
|
||||
"graphs": config.get("graphs", {}),
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -400,6 +442,10 @@ RUN set -ex && \\
|
||||
ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
|
||||
"""
|
||||
)
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
|
||||
"""
|
||||
return f"""FROM {base_image}:{config['python_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
@@ -445,6 +491,10 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
|
||||
ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
|
||||
"""
|
||||
)
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
|
||||
"""
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
@@ -479,8 +529,9 @@ def config_to_compose(
|
||||
f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
|
||||
)
|
||||
if watch:
|
||||
dependencies = config.get("dependencies") or ["."]
|
||||
watch_paths = [config_path.name] + [
|
||||
dep for dep in config["dependencies"] if dep.startswith(".")
|
||||
dep for dep in dependencies if dep.startswith(".")
|
||||
]
|
||||
watch_actions = "\n".join(
|
||||
f"""- path: {path}
|
||||
|
||||
@@ -13,22 +13,23 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.6.2.post1"
|
||||
version = "4.7.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
|
||||
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
|
||||
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
|
||||
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -384,13 +385,13 @@ trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -398,7 +399,6 @@ anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
@@ -407,17 +407,6 @@ http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
|
||||
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -526,18 +515,18 @@ tests = ["flask (>=2.2.5)", "hypothesis (>=6.79.4)", "pytest (>=7.4.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.21"
|
||||
version = "0.3.24"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
|
||||
{file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
|
||||
{file = "langchain_core-0.3.24-py3-none-any.whl", hash = "sha256:97192552ef882a3dd6ae3b870a180a743801d0137a1159173f51ac555eeb7eec"},
|
||||
{file = "langchain_core-0.3.24.tar.gz", hash = "sha256:460851e8145327f70b70aad7dce2cdbd285e144d14af82b677256b941fc99656"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonpatch = ">=1.33,<2.0"
|
||||
langsmith = ">=0.1.125,<0.2.0"
|
||||
langsmith = ">=0.1.125,<0.3"
|
||||
packaging = ">=23.2,<25"
|
||||
pydantic = [
|
||||
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
|
||||
@@ -549,29 +538,29 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.2.53"
|
||||
version = "0.2.59"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9.0"
|
||||
files = [
|
||||
{file = "langgraph-0.2.53-py3-none-any.whl", hash = "sha256:b34b67d0a12ae0ba6f03af97ad0f744bc609bd0328e8b734618cc039985cfdea"},
|
||||
{file = "langgraph-0.2.53.tar.gz", hash = "sha256:b83232a04f2b536cbeac542f9ad7e0265f41ac6b7c6706ba8e031e0e80cb13a6"},
|
||||
{file = "langgraph-0.2.59-py3-none-any.whl", hash = "sha256:9b2d1331bbdcea96cfffde8e88700776118f4583d15f6a3423fc4482fe84ae56"},
|
||||
{file = "langgraph-0.2.59.tar.gz", hash = "sha256:61cb5dd409878641fe8a12e9d7928a7524caa3e54ab12bc883feeaba4a5ba618"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.4.0"
|
||||
langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.3.15 || >0.3.15,<0.3.16 || >0.3.16,<0.3.17 || >0.3.17,<0.3.18 || >0.3.18,<0.3.19 || >0.3.19,<0.3.20 || >0.3.20,<0.3.21 || >0.3.21,<0.3.22 || >0.3.22,<0.4.0"
|
||||
langgraph-checkpoint = ">=2.0.4,<3.0.0"
|
||||
langgraph-sdk = ">=0.1.32,<0.2.0"
|
||||
langgraph-sdk = ">=0.1.42,<0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.0.6"
|
||||
version = "0.0.8"
|
||||
description = ""
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.11.0"
|
||||
files = [
|
||||
{file = "langgraph_api-0.0.6-py3-none-any.whl", hash = "sha256:f64b13959d721143f6a023af5b9ffc9aa054064af98d21d5d8090cda7e7bffd2"},
|
||||
{file = "langgraph_api-0.0.6.tar.gz", hash = "sha256:badac44fa1ec979509e56fc0da57eeb5f278ee5871f27803f73ea6d8822c21b9"},
|
||||
{file = "langgraph_api-0.0.8-py3-none-any.whl", hash = "sha256:ab9b4a5ec8393d17ef921b94f3f019f49f3f17910ba9351beb1e1f962b02a33e"},
|
||||
{file = "langgraph_api-0.0.8.tar.gz", hash = "sha256:b1304beb9d72392ce5d5602cd524a4f57839f1dcd1da6f4e1391832fb4a6edc5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -579,7 +568,7 @@ cryptography = ">=43.0.3,<44.0.0"
|
||||
httpx = ">=0.27.0"
|
||||
jsonschema-rs = ">=0.25.0,<0.26.0"
|
||||
langchain-core = ">=0.2.38,<0.4.0"
|
||||
langgraph = ">=0.2.52,<0.3.0"
|
||||
langgraph = ">=0.2.56,<0.3.0"
|
||||
langgraph-checkpoint = ">=2.0.7,<3.0"
|
||||
langsmith = ">=0.1.63,<0.2.0"
|
||||
orjson = ">=3.10.1"
|
||||
@@ -593,13 +582,13 @@ watchfiles = ">=0.13"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.7"
|
||||
version = "2.0.9"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
files = [
|
||||
{file = "langgraph_checkpoint-2.0.7-py3-none-any.whl", hash = "sha256:9709f672e1c5a47e13352067c2ffa114dd91d443967b7ce8a1d36d6fc170370e"},
|
||||
{file = "langgraph_checkpoint-2.0.7.tar.gz", hash = "sha256:88d648a331d20aa8ce65280de34a34a9190380b004f6afcc5f9894fe3abeed08"},
|
||||
{file = "langgraph_checkpoint-2.0.9-py3-none-any.whl", hash = "sha256:b546ed6129929b8941ac08af6ce5cd26c8ebe1d25883d3c48638d34ade91ce42"},
|
||||
{file = "langgraph_checkpoint-2.0.9.tar.gz", hash = "sha256:43847d7e385a2d9d2b684155920998e44ed42d2d1780719e4f6111fe3d6db84c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -608,18 +597,17 @@ msgpack = ">=1.1.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.40"
|
||||
version = "0.1.44"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
files = [
|
||||
{file = "langgraph_sdk-0.1.40-py3-none-any.whl", hash = "sha256:8810cca5e4144cf3a5441fc76b4ee6e658ec95f932d3a0bf9ad63de117e925b9"},
|
||||
{file = "langgraph_sdk-0.1.40.tar.gz", hash = "sha256:ab2719ac7274612a791a7a0ad9395d250357106cba8ba81bca9968fc91009af2"},
|
||||
{file = "langgraph_sdk-0.1.44-py3-none-any.whl", hash = "sha256:a5a623429b44616c4369c0caf2ea30ce8d3a103123f7bb3ca50dc726eaa313a4"},
|
||||
{file = "langgraph_sdk-0.1.44.tar.gz", hash = "sha256:5b17fe7c0a0fe4b83170c9b8987e0f98dc0636fcc0166dbdba725cc8ffd9b1cf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
httpx = ">=0.25.2"
|
||||
httpx-sse = ">=0.4.0"
|
||||
orjson = ">=3.10.1"
|
||||
|
||||
[[package]]
|
||||
@@ -906,13 +894,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.2"
|
||||
version = "2.10.3"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
|
||||
{file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
|
||||
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
|
||||
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1474,82 +1462,82 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.0.0"
|
||||
version = "1.0.3"
|
||||
description = "Simple, modern and high performance file watching and code reload in python."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2"},
|
||||
{file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935"},
|
||||
{file = "watchfiles-1.0.0-cp310-none-win32.whl", hash = "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755"},
|
||||
{file = "watchfiles-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a"},
|
||||
{file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d"},
|
||||
{file = "watchfiles-1.0.0-cp311-none-win32.whl", hash = "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62"},
|
||||
{file = "watchfiles-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84"},
|
||||
{file = "watchfiles-1.0.0-cp311-none-win_arm64.whl", hash = "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81"},
|
||||
{file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00"},
|
||||
{file = "watchfiles-1.0.0-cp312-none-win32.whl", hash = "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c"},
|
||||
{file = "watchfiles-1.0.0-cp312-none-win_amd64.whl", hash = "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545"},
|
||||
{file = "watchfiles-1.0.0-cp312-none-win_arm64.whl", hash = "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb"},
|
||||
{file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6"},
|
||||
{file = "watchfiles-1.0.0-cp313-none-win32.whl", hash = "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98"},
|
||||
{file = "watchfiles-1.0.0-cp313-none-win_amd64.whl", hash = "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919"},
|
||||
{file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b"},
|
||||
{file = "watchfiles-1.0.0-cp39-none-win32.whl", hash = "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3"},
|
||||
{file = "watchfiles-1.0.0-cp39-none-win_amd64.whl", hash = "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855"},
|
||||
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b"},
|
||||
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d"},
|
||||
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7"},
|
||||
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0"},
|
||||
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e"},
|
||||
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c"},
|
||||
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17"},
|
||||
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc"},
|
||||
{file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da46bb1eefb5a37a8fb6fd52ad5d14822d67c498d99bda8754222396164ae42"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b961b86cd3973f5822826017cad7f5a75795168cb645c3a6b30c349094e02e3"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34e87c7b3464d02af87f1059fedda5484e43b153ef519e4085fe1a03dd94801e"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9dd2b89a16cf7ab9c1170b5863e68de6bf83db51544875b25a5f05a7269e678"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b4691234d31686dca133c920f94e478b548a8e7c750f28dbbc2e4333e0d3da9"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90b0fe1fcea9bd6e3084b44875e179b4adcc4057a3b81402658d0eb58c98edf8"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b90651b4cf9e158d01faa0833b073e2e37719264bcee3eac49fc3c74e7d304b"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2e9fe695ff151b42ab06501820f40d01310fbd58ba24da8923ace79cf6d702d"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62691f1c0894b001c7cde1195c03b7801aaa794a837bd6eef24da87d1542838d"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:275c1b0e942d335fccb6014d79267d1b9fa45b5ac0639c297f1e856f2f532552"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-win32.whl", hash = "sha256:06ce08549e49ba69ccc36fc5659a3d0ff4e3a07d542b895b8a9013fcab46c2dc"},
|
||||
{file = "watchfiles-1.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f280b02827adc9d87f764972fbeb701cf5611f80b619c20568e1982a277d6146"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ffe709b1d0bc2e9921257569675674cafb3a5f8af689ab9f3f2b3f88775b960f"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:418c5ce332f74939ff60691e5293e27c206c8164ce2b8ce0d9abf013003fb7fe"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f492d2907263d6d0d52f897a68647195bc093dafed14508a8d6817973586b6b"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c9f3bc90c556a854f4cab6a79c16974099ccfa3e3e150673d82d47a4bc92c9"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75d3bcfa90454dba8df12adc86b13b6d85fda97d90e708efc036c2760cc6ba44"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5691340f259b8f76b45fb31b98e594d46c36d1dc8285efa7975f7f50230c9093"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e263cc718545b7f897baeac1f00299ab6fabe3e18caaacacb0edf6d5f35513c"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6cf7709ed3e55704cc06f6e835bf43c03bc8e3cb8ff946bf69a2e0a78d9d77"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:703aa5e50e465be901e0e0f9d5739add15e696d8c26c53bc6fc00eb65d7b9469"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bfcae6aecd9e0cb425f5145afee871465b98b75862e038d42fe91fd753ddd780"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-win32.whl", hash = "sha256:6a76494d2c5311584f22416c5a87c1e2cb954ff9b5f0988027bc4ef2a8a67181"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:cf745cbfad6389c0e331786e5fe9ae3f06e9d9c2ce2432378e1267954793975c"},
|
||||
{file = "watchfiles-1.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:2dcc3f60c445f8ce14156854a072ceb36b83807ed803d37fdea2a50e898635d6"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:93436ed550e429da007fbafb723e0769f25bae178fbb287a94cb4ccdf42d3af3"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c18f3502ad0737813c7dad70e3e1cc966cc147fbaeef47a09463bbffe70b0a00"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5bc3ca468bb58a2ef50441f953e1f77b9a61bd1b8c347c8223403dc9b4ac9a"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d1ec043f02ca04bf21b1b32cab155ce90c651aaf5540db8eb8ad7f7e645cba8"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f58d3bfafecf3d81c15d99fc0ecf4319e80ac712c77cf0ce2661c8cf8bf84066"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1df924ba82ae9e77340101c28d56cbaff2c991bd6fe8444a545d24075abb0a87"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:632a52dcaee44792d0965c17bdfe5dc0edad5b86d6a29e53d6ad4bf92dc0ff49"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bf4b459d94a0387617a1b499f314aa04d8a64b7a0747d15d425b8c8b151da0"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca94c85911601b097d53caeeec30201736ad69a93f30d15672b967558df02885"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65ab1fb635476f6170b07e8e21db0424de94877e4b76b7feabfe11f9a5fc12b5"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-win32.whl", hash = "sha256:49bc1bc26abf4f32e132652f4b3bfeec77d8f8f62f57652703ef127e85a3e38d"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:48681c86f2cb08348631fed788a116c89c787fdf1e6381c5febafd782f6c3b44"},
|
||||
{file = "watchfiles-1.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:9e080cf917b35b20c889225a13f290f2716748362f6071b859b60b8847a6aa43"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e153a690b7255c5ced17895394b4f109d5dcc2a4f35cb809374da50f0e5c456a"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac1be85fe43b4bf9a251978ce5c3bb30e1ada9784290441f5423a28633a958a7"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec98e31e1844eac860e70d9247db9d75440fc8f5f679c37d01914568d18721"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0179252846be03fa97d4d5f8233d1c620ef004855f0717712ae1c558f1974a16"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995c374e86fa82126c03c5b4630c4e312327ecfe27761accb25b5e1d7ab50ec8"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b9cb35b7f290db1c31fb2fdf8fc6d3730cfa4bca4b49761083307f441cac5a"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f8dc09ae69af50bead60783180f656ad96bd33ffbf6e7a6fce900f6d53b08f1"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489b80812f52a8d8c7b0d10f0d956db0efed25df2821c7a934f6143f76938bd6"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:228e2247de583475d4cebf6b9af5dc9918abb99d1ef5ee737155bb39fb33f3c0"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1550be1a5cb3be08a3fb84636eaafa9b7119b70c71b0bed48726fd1d5aa9b868"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-win32.whl", hash = "sha256:16db2d7e12f94818cbf16d4c8938e4d8aaecee23826344addfaaa671a1527b07"},
|
||||
{file = "watchfiles-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:160eff7d1267d7b025e983ca8460e8cc67b328284967cbe29c05f3c3163711a3"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c05b021f7b5aa333124f2a64d56e4cb9963b6efdf44e8d819152237bbd93ba15"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:310505ad305e30cb6c5f55945858cdbe0eb297fc57378f29bacceb534ac34199"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddff3f8b9fa24a60527c137c852d0d9a7da2a02cf2151650029fdc97c852c974"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46e86ed457c3486080a72bc837300dd200e18d08183f12b6ca63475ab64ed651"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f79fe7993e230a12172ce7d7c7db061f046f672f2b946431c81aff8f60b2758b"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea2b51c5f38bad812da2ec0cd7eec09d25f521a8b6b6843cbccedd9a1d8a5c15"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fe4e740ea94978b2b2ab308cbf9270a246bcbb44401f77cc8740348cbaeac3d"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af037d3df7188ae21dc1c7624501f2f90d81be6550904e07869d8d0e6766655"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52bb50a4c4ca2a689fdba84ba8ecc6a4e6210f03b6af93181bb61c4ec3abaf86"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c14a07bdb475eb696f85c715dbd0f037918ccbb5248290448488a0b4ef201aad"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-win32.whl", hash = "sha256:be37f9b1f8934cd9e7eccfcb5612af9fb728fecbe16248b082b709a9d1b348bf"},
|
||||
{file = "watchfiles-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ef9ec8068cf23458dbf36a08e0c16f0a2df04b42a8827619646637be1769300a"},
|
||||
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:84fac88278f42d61c519a6c75fb5296fd56710b05bbdcc74bdf85db409a03780"},
|
||||
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c68be72b1666d93b266714f2d4092d78dc53bd11cf91ed5a3c16527587a52e29"},
|
||||
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889a37e2acf43c377b5124166bece139b4c731b61492ab22e64d371cce0e6e80"},
|
||||
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca05cacf2e5c4a97d02a2878a24020daca21dbb8823b023b978210a75c79098"},
|
||||
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8af4b582d5fc1b8465d1d2483e5e7b880cc1a4e99f6ff65c23d64d070867ac58"},
|
||||
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:127de3883bdb29dbd3b21f63126bb8fa6e773b74eaef46521025a9ce390e1073"},
|
||||
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713f67132346bdcb4c12df185c30cf04bdf4bf6ea3acbc3ace0912cab6b7cb8c"},
|
||||
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd85de513eb83f5ec153a802348e7a5baa4588b818043848247e3e8986094e8"},
|
||||
{file = "watchfiles-1.0.3.tar.gz", hash = "sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1561,4 +1549,4 @@ inmem = ["langgraph-api", "python-dotenv"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "8eaaa66d9e6e447699e3bcee336dfe779b58c956f8c2ad6678008a07be935838"
|
||||
content-hash = "c36514f708e8b2b32c20ad9b7a3ba6fca41f043ced0149f0aae4eef43fc6c10b"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.61"
|
||||
version = "0.1.64"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.0.6,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.0.8,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -31,6 +31,7 @@ def test_validate_config():
|
||||
"dockerfile_lines": [],
|
||||
"env": {},
|
||||
"store": None,
|
||||
"auth": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -48,6 +49,7 @@ def test_validate_config():
|
||||
},
|
||||
"env": env,
|
||||
"store": None,
|
||||
"auth": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Optional, Sequence, Type, TypeVar
|
||||
from typing import Any, Generic, Optional, Sequence, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -13,7 +13,7 @@ C = TypeVar("C")
|
||||
class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
__slots__ = ("key", "typ")
|
||||
|
||||
def __init__(self, typ: Type[Any], key: str = "") -> None:
|
||||
def __init__(self, typ: Any, key: str = "") -> None:
|
||||
self.typ = typ
|
||||
self.key = key
|
||||
|
||||
|
||||
@@ -40,12 +40,16 @@ SCHEDULED = sys.intern("__scheduled__")
|
||||
# marker to signal node was scheduled (in distributed mode)
|
||||
TASKS = sys.intern("__pregel_tasks")
|
||||
# for Send objects returned by nodes/edges, corresponds to PUSH below
|
||||
RETURN = sys.intern("__return__")
|
||||
# for writes of a task where we simply record the return value
|
||||
|
||||
# --- Reserved config.configurable keys ---
|
||||
CONFIG_KEY_SEND = sys.intern("__pregel_send")
|
||||
# holds the `write` function that accepts writes to state/edges/reserved keys
|
||||
CONFIG_KEY_READ = sys.intern("__pregel_read")
|
||||
# holds the `read` function that returns a copy of the current state
|
||||
CONFIG_KEY_CALL = sys.intern("__pregel_call")
|
||||
# holds the `call` function that accepts a node/func, args and returns a future
|
||||
CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer")
|
||||
# holds a `BaseCheckpointSaver` passed from parent graph to child graphs
|
||||
CONFIG_KEY_STREAM = sys.intern("__pregel_stream")
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import asyncio
|
||||
import concurrent
|
||||
import concurrent.futures
|
||||
import inspect
|
||||
import types
|
||||
from functools import partial, update_wrapper
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, START, TAG_HIDDEN
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.pregel.call import get_runnable_for_func
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import RetryPolicy, StreamMode, StreamWriter
|
||||
|
||||
P = ParamSpec("P")
|
||||
P1 = TypeVar("P1")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def call(
|
||||
func: Callable[[P1], T],
|
||||
input: P1,
|
||||
*,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
) -> concurrent.futures.Future[T]:
|
||||
from langgraph.constants import CONFIG_KEY_CALL
|
||||
from langgraph.utils.config import get_configurable
|
||||
|
||||
conf = get_configurable()
|
||||
impl = conf[CONFIG_KEY_CALL]
|
||||
fut = impl(func, input, retry=retry)
|
||||
return fut
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
*, retry: Optional[RetryPolicy] = None
|
||||
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task( # type: ignore[overload-cannot-match]
|
||||
*, retry: Optional[RetryPolicy] = None
|
||||
) -> Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]]: ...
|
||||
|
||||
|
||||
def task(
|
||||
*, retry: Optional[RetryPolicy] = None
|
||||
) -> Union[
|
||||
Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]],
|
||||
Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]],
|
||||
]:
|
||||
def _task(func: Callable[P, T]) -> Callable[P, concurrent.futures.Future[T]]:
|
||||
return update_wrapper(partial(call, func, retry=retry), func)
|
||||
|
||||
return _task
|
||||
|
||||
|
||||
def entrypoint(
|
||||
*,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
) -> Callable[[types.FunctionType], Pregel]:
|
||||
def _imp(func: types.FunctionType) -> Pregel:
|
||||
if inspect.isgeneratorfunction(func):
|
||||
|
||||
def gen_wrapper(*args: Any, writer: StreamWriter, **kwargs: Any) -> Any:
|
||||
for chunk in func(*args, **kwargs):
|
||||
writer(chunk)
|
||||
|
||||
bound = get_runnable_for_func(gen_wrapper)
|
||||
stream_mode: StreamMode = "custom"
|
||||
elif inspect.isasyncgenfunction(func):
|
||||
|
||||
async def agen_wrapper(
|
||||
*args: Any, writer: StreamWriter, **kwargs: Any
|
||||
) -> Any:
|
||||
async for chunk in func(*args, **kwargs):
|
||||
writer(chunk)
|
||||
|
||||
bound = get_runnable_for_func(agen_wrapper)
|
||||
stream_mode = "custom"
|
||||
else:
|
||||
bound = get_runnable_for_func(func)
|
||||
stream_mode = "updates"
|
||||
|
||||
return Pregel(
|
||||
nodes={
|
||||
func.__name__: PregelNode(
|
||||
bound=bound,
|
||||
triggers=[START],
|
||||
channels=[START],
|
||||
writers=[ChannelWrite([ChannelWriteEntry(END)], tags=[TAG_HIDDEN])],
|
||||
)
|
||||
},
|
||||
channels={START: EphemeralValue(Any), END: LastValue(Any, END)},
|
||||
input_channels=START,
|
||||
output_channels=END,
|
||||
stream_channels=END,
|
||||
stream_mode=stream_mode,
|
||||
checkpointer=checkpointer,
|
||||
store=store,
|
||||
)
|
||||
|
||||
return _imp
|
||||