mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
808
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4f335ffe | ||
|
|
9c36a7de85 | ||
|
|
8e922b859c | ||
|
|
9a4c30135f | ||
|
|
bc6651f34c | ||
|
|
23bb5369b9 | ||
|
|
85b81371f7 | ||
|
|
54ab833b74 | ||
|
|
4ea936eaf4 | ||
|
|
9d81ec9ffd | ||
|
|
46b652a74c | ||
|
|
7013ca9a3f | ||
|
|
0c04aec664 | ||
|
|
1650c8508e | ||
|
|
e176b98fe7 | ||
|
|
eb1e1aa010 | ||
|
|
77c833e1e5 | ||
|
|
0ac29434a7 | ||
|
|
43f5a17416 | ||
|
|
7d0857f263 | ||
|
|
b82d70a66a | ||
|
|
5fb037171d | ||
|
|
d3bb2b9aa0 | ||
|
|
ea765b4134 | ||
|
|
66ff83dca9 | ||
|
|
254e398345 | ||
|
|
c7567ea219 | ||
|
|
8c0306c3f4 | ||
|
|
992b05a196 | ||
|
|
893a9646d3 | ||
|
|
eaa37a2ce9 | ||
|
|
daee8d88bb | ||
|
|
eaa18cc2dd | ||
|
|
b2d9a36308 | ||
|
|
03fc695d60 | ||
|
|
9994b09304 | ||
|
|
53f8558914 | ||
|
|
4bfcd84cee | ||
|
|
cd1d7be05f | ||
|
|
939a426a2e | ||
|
|
94c815f226 | ||
|
|
ef345aac5f | ||
|
|
e306258525 | ||
|
|
05cd317486 | ||
|
|
6dfed31a5e | ||
|
|
ee8374c4c0 | ||
|
|
2066894b5f | ||
|
|
6a2d20fd5b | ||
|
|
5b8b9f1067 | ||
|
|
c9cb8165d4 | ||
|
|
1f0348a5ca | ||
|
|
442ef0788e | ||
|
|
73f9ef0ef8 | ||
|
|
1f7a380548 | ||
|
|
8959f2aec5 | ||
|
|
0e7869eba4 | ||
|
|
d3f8478054 | ||
|
|
0cb1893475 | ||
|
|
e779c8e0b1 | ||
|
|
18b82cb8e2 | ||
|
|
972ab1a935 | ||
|
|
9cc2f37cca | ||
|
|
a2d7631f47 | ||
|
|
1e767c0653 | ||
|
|
d4c569cb7c | ||
|
|
a146df7f6a | ||
|
|
c52cc03e4b | ||
|
|
f206cfad8f | ||
|
|
d4c8b219c4 | ||
|
|
00855999d2 | ||
|
|
24bd0e1c1f | ||
|
|
3b59055192 | ||
|
|
67a16bec53 | ||
|
|
f97802eed5 | ||
|
|
bccb796ccc | ||
|
|
4068e9d135 | ||
|
|
a951334f7f | ||
|
|
75a727877f | ||
|
|
1cece3228c | ||
|
|
7ba48d75c9 | ||
|
|
7fb0628957 | ||
|
|
6edf29f043 | ||
|
|
c8a605cbc8 | ||
|
|
fbec207446 | ||
|
|
2223c82606 | ||
|
|
06f2eef74c | ||
|
|
62aa66cd4b | ||
|
|
8ffe9634b7 | ||
|
|
4b1d6d2aeb | ||
|
|
199ab46429 | ||
|
|
c758954519 | ||
|
|
5bfb3bb882 | ||
|
|
68a5c3f4c7 | ||
|
|
b7fb8e6afb | ||
|
|
c34c798763 | ||
|
|
792cd805a7 | ||
|
|
764929afd9 | ||
|
|
1e751a2256 | ||
|
|
e6726802f7 | ||
|
|
a541376d10 | ||
|
|
f9780330a6 | ||
|
|
24f7d7c439 | ||
|
|
9533d35a84 | ||
|
|
48de4a7234 | ||
|
|
67ac35c5bc | ||
|
|
12c7ddf3a3 | ||
|
|
d6caa3b00a | ||
|
|
3d48526c16 | ||
|
|
2f3bd69bf5 | ||
|
|
f10a0c6f32 | ||
|
|
2eba27b01f | ||
|
|
c0245a6ee9 | ||
|
|
9e82d23252 | ||
|
|
460c522902 | ||
|
|
298a19b573 | ||
|
|
d2ec46b927 | ||
|
|
22e4bf74fc | ||
|
|
96a0536ec4 | ||
|
|
8d33938173 | ||
|
|
1c73b1e45a | ||
|
|
465d5d648b | ||
|
|
b751e8bcee | ||
|
|
8ec3982056 | ||
|
|
f17f264a7a | ||
|
|
086443472f | ||
|
|
936e69404e | ||
|
|
b1a25abc73 | ||
|
|
0e70b8d94f | ||
|
|
e1aa1a4510 | ||
|
|
ae7dbd1fa5 | ||
|
|
3ec95153ce | ||
|
|
4836f8b18b | ||
|
|
e7fbdeeb13 | ||
|
|
ee650ab85f | ||
|
|
7a959f62cc | ||
|
|
82905297fd | ||
|
|
9b5549f759 | ||
|
|
fa96c0ac76 | ||
|
|
98b8ff904c | ||
|
|
951131c8ec | ||
|
|
8bcdba822e | ||
|
|
60fc49b448 | ||
|
|
1d21b4ba08 | ||
|
|
55ec0d3d2a | ||
|
|
c7dd7be030 | ||
|
|
47d38a3022 | ||
|
|
8e829f38af | ||
|
|
3f241d00a3 | ||
|
|
f0abf582dd | ||
|
|
2458f2d2e0 | ||
|
|
477a43dae0 | ||
|
|
fc8e6ec64f | ||
|
|
d6a457ef1d | ||
|
|
ce1077da40 | ||
|
|
969958695a | ||
|
|
dd16ae4ba5 | ||
|
|
e24e141253 | ||
|
|
eae1faa656 | ||
|
|
1976d6584c | ||
|
|
54e18445fc | ||
|
|
69dc29aaf9 | ||
|
|
aa5ff74845 | ||
|
|
6049aaa842 | ||
|
|
576aa1ca02 | ||
|
|
e28e97d5e0 | ||
|
|
59e7c63c93 | ||
|
|
be7dee1c3b | ||
|
|
0aafa04bac | ||
|
|
2e1adaa867 | ||
|
|
3f8b165592 | ||
|
|
9ed0fa196c | ||
|
|
0b9adc28c3 | ||
|
|
3b0255d1ef | ||
|
|
80c3ccba7b | ||
|
|
d4255a0645 | ||
|
|
80d61a2600 | ||
|
|
424f24720a | ||
|
|
2a71180c1d | ||
|
|
697f878e36 | ||
|
|
987b9da4ab | ||
|
|
4bff1df4b0 | ||
|
|
5104e31e35 | ||
|
|
7ae4739630 | ||
|
|
5db1949ae3 | ||
|
|
ddb29df667 | ||
|
|
fa467573d7 | ||
|
|
1a728a93c6 | ||
|
|
def69c59d2 | ||
|
|
aaa0cd6b51 | ||
|
|
a9c831c11b | ||
|
|
bad4d17c34 | ||
|
|
55219b23d8 | ||
|
|
8edbd39ad3 | ||
|
|
4b0fd834d8 | ||
|
|
0fd2748530 | ||
|
|
bc0a3419ed | ||
|
|
5cd47bac49 | ||
|
|
4c6d80a67f | ||
|
|
18ed044c27 | ||
|
|
394a9fa85f | ||
|
|
9741d9bdf0 | ||
|
|
06ca07432d | ||
|
|
e757a80001 | ||
|
|
a204444905 | ||
|
|
4cfdf8774a | ||
|
|
beb62fc053 | ||
|
|
857f3e4a38 | ||
|
|
6342cd1665 | ||
|
|
baedf91836 | ||
|
|
190b42850f | ||
|
|
678b512aed | ||
|
|
c85e246c32 | ||
|
|
98ebc45f31 | ||
|
|
5ca2f358f9 | ||
|
|
86169c1439 | ||
|
|
36d6eed468 | ||
|
|
ef50fed6fe | ||
|
|
c0abfc7df6 | ||
|
|
318889bc6c | ||
|
|
b9e3fd5f3e | ||
|
|
8729ebc40c | ||
|
|
919282fead | ||
|
|
cff4784ff8 | ||
|
|
9921e5210a | ||
|
|
048eff9f11 | ||
|
|
8a2765c8f2 | ||
|
|
ffbcdd1ecc | ||
|
|
108a041fa7 | ||
|
|
52e3c59f07 | ||
|
|
0e17988332 | ||
|
|
5f0d05099d | ||
|
|
a1739d3184 | ||
|
|
ab38cc2cc0 | ||
|
|
647833dcaa | ||
|
|
1f03735b7d | ||
|
|
d980cca59b | ||
|
|
944b93bf61 | ||
|
|
8aa59d002a | ||
|
|
ed533b32a8 | ||
|
|
d88f59eea4 | ||
|
|
fc5dde6c55 | ||
|
|
4c902d21a3 | ||
|
|
54804af06a | ||
|
|
312f026e9c | ||
|
|
6973b19cc7 | ||
|
|
45ed67856f | ||
|
|
74b2dbe1ea | ||
|
|
7f803df586 | ||
|
|
a5b43c933a | ||
|
|
aae2fb4b85 | ||
|
|
c20a50875d | ||
|
|
779553f4aa | ||
|
|
537e69608e | ||
|
|
208cd4d70e | ||
|
|
1352e58133 | ||
|
|
f1162ac898 | ||
|
|
4de8443c5c | ||
|
|
96dc39aeab | ||
|
|
316f8410fa | ||
|
|
1d3926af27 | ||
|
|
e566ed4b3f | ||
|
|
14c2241853 | ||
|
|
2c908f1557 | ||
|
|
5005d1c004 | ||
|
|
02a46c45c8 | ||
|
|
852a129881 | ||
|
|
78348d2d9f | ||
|
|
44af8d5257 | ||
|
|
84c956bc8c | ||
|
|
b86e6b82f2 | ||
|
|
b1de5be334 | ||
|
|
0751428422 | ||
|
|
b16f05405b | ||
|
|
ca8d92421a | ||
|
|
7aa9d3fd00 | ||
|
|
ed69f60f24 | ||
|
|
c55f1f12bf | ||
|
|
5d76b1d624 | ||
|
|
857fd3578f | ||
|
|
3a4af1e573 | ||
|
|
37344124e1 | ||
|
|
d0f4db6ddd | ||
|
|
a9800aab87 | ||
|
|
9bf3fc2d0f | ||
|
|
a6e4bd93ff | ||
|
|
0e9c41f480 | ||
|
|
d4368cfa97 | ||
|
|
3808302309 | ||
|
|
25019450e2 | ||
|
|
e4c7db180e | ||
|
|
3183146141 | ||
|
|
f070b1c805 | ||
|
|
141589a7e6 | ||
|
|
be37631181 | ||
|
|
51ddc792d6 | ||
|
|
52d4f73e39 | ||
|
|
4b25e28e3e | ||
|
|
263eab9f76 | ||
|
|
c3df8bd500 | ||
|
|
d86502421d | ||
|
|
cbf26a5d98 | ||
|
|
09bd5990d4 | ||
|
|
79595d43a5 | ||
|
|
49a6704bdc | ||
|
|
a430b7fcfb | ||
|
|
48c287d107 | ||
|
|
0fdc787597 | ||
|
|
3bd86fc6f5 | ||
|
|
f1024f7341 | ||
|
|
f9ac88012f | ||
|
|
9d0186b5bf | ||
|
|
40062c40df | ||
|
|
36bbe059df | ||
|
|
52627010cb | ||
|
|
f6781d19ab | ||
|
|
fcf134a452 | ||
|
|
80331b88e0 | ||
|
|
b37f894f38 | ||
|
|
79de3dbad3 | ||
|
|
677fd3ce28 | ||
|
|
4ee863f30d | ||
|
|
7e060f88a2 | ||
|
|
f49856af0b | ||
|
|
5a7d384e2f | ||
|
|
b73b34ddd5 | ||
|
|
c9ffd753f5 | ||
|
|
e85e157e8f | ||
|
|
88e7868885 | ||
|
|
e8dd682320 | ||
|
|
75143b966c | ||
|
|
490e1aab3b | ||
|
|
003226cef4 | ||
|
|
098a199cb9 | ||
|
|
97f6f45993 | ||
|
|
e8631c052a | ||
|
|
16a86c8b8e | ||
|
|
5fa6bb5f55 | ||
|
|
d29e9e22c7 | ||
|
|
6990e1fcf5 | ||
|
|
22e60c47cc | ||
|
|
d9396c38ea | ||
|
|
ff60ee8c9a | ||
|
|
8761721fb9 | ||
|
|
de85e7c246 | ||
|
|
d333f4438f | ||
|
|
af14a2abbc | ||
|
|
e466c2c90c | ||
|
|
815a67ef55 | ||
|
|
38f1b415a0 | ||
|
|
ed78174adf | ||
|
|
5da6971a95 | ||
|
|
256e92bfb3 | ||
|
|
3d4e5c0471 | ||
|
|
013a12334e | ||
|
|
c7211e03e9 | ||
|
|
ffc916e38c | ||
|
|
03bf149ebd | ||
|
|
137dcce5b5 | ||
|
|
48164a95da | ||
|
|
3926e83884 | ||
|
|
43709a16bf | ||
|
|
d98c7248dc | ||
|
|
ac2736f18e | ||
|
|
fc130a52ef | ||
|
|
7d025e42ef | ||
|
|
4e9ed36f76 | ||
|
|
5d73df6133 | ||
|
|
fcc1210945 | ||
|
|
da5ee30bef | ||
|
|
57ff761cff | ||
|
|
f0a46bc3e3 | ||
|
|
b1587d24ed | ||
|
|
a1c676707c | ||
|
|
38b19fa99c | ||
|
|
3c3428da78 | ||
|
|
bb0125b4bb | ||
|
|
7580ad6005 | ||
|
|
eb8aa6b761 | ||
|
|
fe0de3e07e | ||
|
|
f51831f48e | ||
|
|
745eb90a6d | ||
|
|
f136e40065 | ||
|
|
de888b4032 | ||
|
|
7cb0bd52e8 | ||
|
|
3778f6113c | ||
|
|
24c13c211e | ||
|
|
2e36189c16 | ||
|
|
302aa8b9cb | ||
|
|
dac11f875c | ||
|
|
264be423f9 | ||
|
|
9a05600ff9 | ||
|
|
56dd728975 | ||
|
|
c157c956f4 | ||
|
|
dd293dad30 | ||
|
|
30811d7841 | ||
|
|
28a705b71a | ||
|
|
3b8130b96f | ||
|
|
94fc0adb05 | ||
|
|
162e96262f | ||
|
|
1bb0037450 | ||
|
|
50c53d3120 | ||
|
|
0b7b849633 | ||
|
|
678eb5cdbe | ||
|
|
a0969b61a3 | ||
|
|
e679ab73c4 | ||
|
|
b5a981d82d | ||
|
|
bdf1215ced | ||
|
|
f679348327 | ||
|
|
cba1852720 | ||
|
|
edf707be51 | ||
|
|
d3b9a96504 | ||
|
|
3257e5ae76 | ||
|
|
bedd0eb286 | ||
|
|
42f0c351fd | ||
|
|
a290984362 | ||
|
|
f1d6fd184f | ||
|
|
503f716104 | ||
|
|
b8fafa2795 | ||
|
|
515c34d1ce | ||
|
|
5ea0d49d4d | ||
|
|
d9f71ef8b3 | ||
|
|
8658a5dc0b | ||
|
|
2afee13d9e | ||
|
|
f7d9daa4eb | ||
|
|
fb28aa6d4b | ||
|
|
3488945cdf | ||
|
|
e6681bc175 | ||
|
|
57e8081921 | ||
|
|
078b335448 | ||
|
|
6615c6bb0d | ||
|
|
32df0016ee | ||
|
|
39b2bb9c8f | ||
|
|
5eb793d7d8 | ||
|
|
a9cdb9c948 | ||
|
|
3dac1894cc | ||
|
|
1500764b46 | ||
|
|
fbb89325f9 | ||
|
|
187c71a812 | ||
|
|
b09b33070e | ||
|
|
31a7bcf750 | ||
|
|
1a12b0309c | ||
|
|
660c15d072 | ||
|
|
4a59da7cfd | ||
|
|
aacc079eed | ||
|
|
3d70a4ed65 | ||
|
|
caad15f7ae | ||
|
|
1f4d4e7bfd | ||
|
|
5b0bf861ac | ||
|
|
a69ea47ac2 | ||
|
|
f83d18188f | ||
|
|
688efdea3d | ||
|
|
6641dcd3c9 | ||
|
|
ad14d92f5e | ||
|
|
c63fbbfaa6 | ||
|
|
577b4413a9 | ||
|
|
277c341817 | ||
|
|
88d0f41c55 | ||
|
|
0a4953c4bc | ||
|
|
059e16789c | ||
|
|
4dc8f813e7 | ||
|
|
f613fdfcbc | ||
|
|
41b36dcf3f | ||
|
|
b39dcd7fad | ||
|
|
c0ad92b6db | ||
|
|
b8d25bc0ed | ||
|
|
7884401ec8 | ||
|
|
a3bc029344 | ||
|
|
4875973ac5 | ||
|
|
15e67fdd57 | ||
|
|
265466184c | ||
|
|
1393270664 | ||
|
|
6e0295b4de | ||
|
|
4aadfccf95 | ||
|
|
70b2da1301 | ||
|
|
209864da45 | ||
|
|
8b6ef35f0c | ||
|
|
a01537d1a5 | ||
|
|
05b4a30c04 | ||
|
|
93e10fbe15 | ||
|
|
f6989f2c7d | ||
|
|
c4f8346479 | ||
|
|
82c9d4b368 | ||
|
|
647f22fdd9 | ||
|
|
0aba1b4887 | ||
|
|
4c0c52d996 | ||
|
|
580fe68c8e | ||
|
|
2f26268ff4 | ||
|
|
84f8a43f6e | ||
|
|
7ce0e3e15e | ||
|
|
1e6d958434 | ||
|
|
68ba8aa393 | ||
|
|
e6a0f08561 | ||
|
|
f38784a291 | ||
|
|
fc834127fd | ||
|
|
f3a0cbf294 | ||
|
|
f4fec76257 | ||
|
|
457641f15e | ||
|
|
9f15e15e26 | ||
|
|
cb9989030e | ||
|
|
a008725c06 | ||
|
|
a93f17e624 | ||
|
|
c7eddcc6e3 | ||
|
|
4cdad6c206 | ||
|
|
b9fb155d59 | ||
|
|
8b817a5b16 | ||
|
|
3a67f3a3eb | ||
|
|
1283539500 | ||
|
|
69ad42cac5 | ||
|
|
264b02e3ad | ||
|
|
b5c659bc9f | ||
|
|
4623f7b5da | ||
|
|
1641402341 | ||
|
|
d03ead2f43 | ||
|
|
5a8624fdfd | ||
|
|
c44ec55095 | ||
|
|
25d682cc9e | ||
|
|
6f37330141 | ||
|
|
1356a0ba42 | ||
|
|
9786be1ff7 | ||
|
|
c2a129c882 | ||
|
|
62f004fd28 | ||
|
|
d4b22ac1d4 | ||
|
|
0415c02b40 | ||
|
|
e8665f84e7 | ||
|
|
8ff5e79e70 | ||
|
|
7f4822931e | ||
|
|
9706211aca | ||
|
|
405da6d507 | ||
|
|
bf7252cadc | ||
|
|
e33bac6737 | ||
|
|
da97d2e1ba | ||
|
|
6baf320d8e | ||
|
|
a064ccdca1 | ||
|
|
b5479b48bf | ||
|
|
9dbcb03185 | ||
|
|
f2faa39ca9 | ||
|
|
b4f6cdf01f | ||
|
|
cd976e779d | ||
|
|
c1f337f50b | ||
|
|
31d3ceaf6d | ||
|
|
a48844632d | ||
|
|
437891aa4f | ||
|
|
3e1bbd3123 | ||
|
|
9111449ffd | ||
|
|
77d7c00ce8 | ||
|
|
91725d742d | ||
|
|
d73a4539ec | ||
|
|
15e2df6da5 | ||
|
|
ce6b396186 | ||
|
|
d2ab02edf1 | ||
|
|
0aef9424a8 | ||
|
|
ed23288e5b | ||
|
|
7ec8a4cb4d | ||
|
|
2d9ca3045e | ||
|
|
b065c54871 | ||
|
|
3c0a677c90 | ||
|
|
7e4852373d | ||
|
|
422b2ba7f0 | ||
|
|
2c66ac869d | ||
|
|
1ef7121100 | ||
|
|
a53287f3d8 | ||
|
|
da96925ecb | ||
|
|
f3403eab48 | ||
|
|
208d9d165d | ||
|
|
49c74dd569 | ||
|
|
2d97af57f8 | ||
|
|
b310ce07bc | ||
|
|
65976f311f | ||
|
|
80c9d61fbd | ||
|
|
a578c7b137 | ||
|
|
04e8342d97 | ||
|
|
b0e11ae524 | ||
|
|
d45253cee8 | ||
|
|
1377e3b6ba | ||
|
|
c36323cba8 | ||
|
|
148cf52981 | ||
|
|
9010303245 | ||
|
|
661476e88d | ||
|
|
3397d8908f | ||
|
|
e005f0472b | ||
|
|
18a1d60e45 | ||
|
|
5091d5e9fe | ||
|
|
da1fae0c72 | ||
|
|
7db81d72dc | ||
|
|
4ce387ee28 | ||
|
|
b902db686d | ||
|
|
16be48079e | ||
|
|
cf3a9ad63e | ||
|
|
547830ef24 | ||
|
|
d333e52fce | ||
|
|
15fe44ddf8 | ||
|
|
b6aff521e5 | ||
|
|
35ff9f5211 | ||
|
|
d690fc3e00 | ||
|
|
3325787af7 | ||
|
|
77a3cffa7f | ||
|
|
d1f2a6c518 | ||
|
|
b984584851 | ||
|
|
bce4545021 | ||
|
|
29c317887d | ||
|
|
84a0eca935 | ||
|
|
d9ed1ef52e | ||
|
|
fda79e00ce | ||
|
|
3163a60466 | ||
|
|
ac05955222 | ||
|
|
f3fe30380c | ||
|
|
2c5ddceeb1 | ||
|
|
98976e016a | ||
|
|
ea8025b719 | ||
|
|
735a76a16c | ||
|
|
a33437964c | ||
|
|
cb9405bcee | ||
|
|
bd2268404c | ||
|
|
25f88740c1 | ||
|
|
e9809ae9c1 | ||
|
|
69311a4135 | ||
|
|
bb3193c83e | ||
|
|
7a2eb614dc | ||
|
|
7fd6931200 | ||
|
|
cbca07e3db | ||
|
|
066525b335 | ||
|
|
a1dad43602 | ||
|
|
1e8f097656 | ||
|
|
db26c915a9 | ||
|
|
9b62280fc5 | ||
|
|
515ad8d7a6 | ||
|
|
d378f0e06a | ||
|
|
740870df65 | ||
|
|
6e20c9f3f9 | ||
|
|
530544234a | ||
|
|
f11d241482 | ||
|
|
e81979827f | ||
|
|
2064ea4793 | ||
|
|
16cfeff78c | ||
|
|
4321d337d6 | ||
|
|
d56e2545a6 | ||
|
|
9a3f96c459 | ||
|
|
cb509ad6a5 | ||
|
|
e479a2c643 | ||
|
|
0caae32a40 | ||
|
|
a6b8098548 | ||
|
|
0631f19ef9 | ||
|
|
e9b0bc4d3b | ||
|
|
eb19b80d13 | ||
|
|
e5ccdd91c2 | ||
|
|
871f15a630 | ||
|
|
32aa87d4b2 | ||
|
|
55a3352e9b | ||
|
|
6adaa6cf78 | ||
|
|
28172a2767 | ||
|
|
028137a51a | ||
|
|
c3847fae6c | ||
|
|
3daa4ae466 | ||
|
|
6a36f4bf91 | ||
|
|
04a0443742 | ||
|
|
3b224df566 | ||
|
|
0a2e2b4796 | ||
|
|
0e0b3ffac5 | ||
|
|
75509bd221 | ||
|
|
f997e5eafd | ||
|
|
6337c62dc7 | ||
|
|
59992fac7c | ||
|
|
28a7ca03a9 | ||
|
|
9fe61a42d2 | ||
|
|
59ae3b873c | ||
|
|
31ba5c41ac | ||
|
|
6ddbdd0638 | ||
|
|
3f7f52ccdc | ||
|
|
ee9a34f726 | ||
|
|
72ce34e3e6 | ||
|
|
098d5ec403 | ||
|
|
45ef425d85 | ||
|
|
8c192414a2 | ||
|
|
c0db7f4d09 | ||
|
|
072f2d43eb | ||
|
|
c02a74e221 | ||
|
|
15af8a9e6c | ||
|
|
1b257b4d0a | ||
|
|
384814036e | ||
|
|
d5b0ad2cf6 | ||
|
|
8d8e514924 | ||
|
|
a37c4d6f49 | ||
|
|
4b3e07b67a | ||
|
|
6ba61cc768 | ||
|
|
5a79210904 | ||
|
|
4a60eaf32f | ||
|
|
cf7c3e7fd1 | ||
|
|
141b53ee6e | ||
|
|
37e8e00f1f | ||
|
|
a0ec9017f2 | ||
|
|
80cef60405 | ||
|
|
46cba763be | ||
|
|
830e5f4550 | ||
|
|
55e9409b6e | ||
|
|
39e65a1a62 | ||
|
|
82148e9bf2 | ||
|
|
ed09a77d9f | ||
|
|
7e267897c9 | ||
|
|
d279902156 | ||
|
|
440158b969 | ||
|
|
0cf3a64d66 | ||
|
|
953e2907d4 | ||
|
|
0a861815b7 | ||
|
|
928126f2d7 | ||
|
|
0d8fea847a | ||
|
|
b4dc3a851f | ||
|
|
7568862013 | ||
|
|
69dc5f326e | ||
|
|
90f1e66748 | ||
|
|
a910a1a341 | ||
|
|
554e763994 | ||
|
|
adff1df813 | ||
|
|
64b828def6 | ||
|
|
9196cc2da8 | ||
|
|
e13a8bc320 | ||
|
|
4b5b309152 | ||
|
|
9d5166a0d3 | ||
|
|
5b11683f4e | ||
|
|
3fae8d47c3 | ||
|
|
be9cb03dfa | ||
|
|
6926c4bcc0 | ||
|
|
c04802a344 | ||
|
|
cf7f6691cf | ||
|
|
e1140f4fad | ||
|
|
ae8afb7677 | ||
|
|
48040d8ea5 | ||
|
|
ad51bfdf71 | ||
|
|
996b120613 | ||
|
|
1093dd55c8 | ||
|
|
d794875b32 | ||
|
|
6948cf5eb8 | ||
|
|
bacc2955ae | ||
|
|
f014d96d1c | ||
|
|
7b552ebf4b | ||
|
|
1e61ddfdbe | ||
|
|
d34846dc08 | ||
|
|
06823e327f | ||
|
|
1059ef55d1 | ||
|
|
39552255c8 | ||
|
|
38bbe67469 | ||
|
|
6335963674 | ||
|
|
8a4c452317 | ||
|
|
5dc5853161 | ||
|
|
9e066554ba | ||
|
|
6ce9354ee4 | ||
|
|
211fd4337d | ||
|
|
44bf97ac0e | ||
|
|
23c73ae719 | ||
|
|
4165d479e9 | ||
|
|
c43a9a4bd0 | ||
|
|
c9613927dc | ||
|
|
c697c2aa04 | ||
|
|
3f2557c9c9 | ||
|
|
cbad17fa7d | ||
|
|
17dacb83a2 | ||
|
|
3a997be088 | ||
|
|
020d10138d | ||
|
|
303587c4ff | ||
|
|
7d4e636313 | ||
|
|
86913caf89 | ||
|
|
041faefe29 | ||
|
|
b704cf30cc | ||
|
|
3915b44180 | ||
|
|
ac1407b23c | ||
|
|
44840aa23f | ||
|
|
51242e2a32 | ||
|
|
b358e2e7cd | ||
|
|
0d91ab1474 | ||
|
|
12ae297194 | ||
|
|
0177565c6b | ||
|
|
c48d495031 | ||
|
|
22e6468af5 | ||
|
|
24bc0c0630 | ||
|
|
802e6df8df | ||
|
|
3ec55b008d | ||
|
|
e10b7c1391 | ||
|
|
31cc6b9f1d | ||
|
|
2cafb4905b | ||
|
|
fe46576d98 | ||
|
|
16c86c9de6 | ||
|
|
fca0d2d5bb | ||
|
|
c7c62f5587 | ||
|
|
956c5f68fc | ||
|
|
4908caf522 | ||
|
|
f926eada24 | ||
|
|
adc1e47028 | ||
|
|
7820b5c765 | ||
|
|
ca6f8e2042 | ||
|
|
ee61d06f8d | ||
|
|
6eeb9de46a | ||
|
|
19a91f4677 | ||
|
|
6087b1969e | ||
|
|
6cbc7e8b67 | ||
|
|
b2213e523e | ||
|
|
b7c3ac4501 | ||
|
|
09e8516689 | ||
|
|
3ca75d69b8 | ||
|
|
d48dec5452 | ||
|
|
d48b25420b | ||
|
|
12be3fac33 | ||
|
|
aed1f0ba18 | ||
|
|
07695f5c5a | ||
|
|
204c9c83f8 | ||
|
|
31578cbe0e | ||
|
|
44970640d5 | ||
|
|
0f4e42474f | ||
|
|
832f9ad64e | ||
|
|
318de5bb81 |
@@ -54,7 +54,7 @@ jobs:
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry lock --check
|
||||
run: poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
|
||||
@@ -39,6 +39,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
|
||||
|
||||
- name: Check Lock
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -20,7 +20,31 @@ env:
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
sdk-js: ${{ steps.filter.outputs.sdk-js }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
python:
|
||||
- 'libs/langgraph/**'
|
||||
- 'libs/sdk-py/**'
|
||||
- 'libs/cli/**'
|
||||
- 'libs/checkpoint/**'
|
||||
- 'libs/checkpoint-sqlite/**'
|
||||
- 'libs/checkpoint-postgres/**'
|
||||
- 'libs/scheduler-kafka/**'
|
||||
- 'libs/prebuilt/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
|
||||
lint:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -33,13 +57,16 @@ jobs:
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/scheduler-kafka",
|
||||
"libs/prebuilt",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_lint.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -49,7 +76,9 @@ jobs:
|
||||
"libs/checkpoint",
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/prebuilt",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_test.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
@@ -57,17 +86,23 @@ jobs:
|
||||
|
||||
# NOTE: we're testing langgraph separately because it requires a different matrix
|
||||
test-langgraph:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/langgraph"
|
||||
uses: ./.github/workflows/_test_langgraph.yml
|
||||
secrets: inherit
|
||||
|
||||
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
|
||||
test-scheduler-kafka:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/scheduler-kafka"
|
||||
uses: ./.github/workflows/_test_scheduler_kafka.yml
|
||||
secrets: inherit
|
||||
|
||||
check-sdk-methods:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "Check SDK methods matching"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -79,12 +114,52 @@ jobs:
|
||||
- name: Run check_sdk_methods script
|
||||
run: python .github/scripts/check_sdk_methods.py
|
||||
|
||||
check-schema:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "Check CLI schema hasn't changed #${{ matrix.python-version }}"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
cache-key: schema-check-cli
|
||||
- name: Install CLI dependencies
|
||||
run: |
|
||||
cd libs/cli
|
||||
poetry install
|
||||
- name: Generate schema and check for changes
|
||||
run: |
|
||||
cd libs/cli
|
||||
# Create a temporary copy of the current schema
|
||||
cp schemas/schema.json schemas/schema.current.json
|
||||
# Generate new schema
|
||||
poetry run python generate_schema.py
|
||||
# Compare the new schema with the original
|
||||
if ! diff -q schemas/schema.json schemas/schema.current.json > /dev/null; then
|
||||
echo "Error: Langgraph.json configuration schema has changed. Please run 'poetry run python generate_schema.py' in the libs/cli directory and commit the changes."
|
||||
diff schemas/schema.json schemas/schema.current.json
|
||||
exit 1
|
||||
fi
|
||||
echo "Schema check passed - no changes detected"
|
||||
|
||||
integration-test:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: CLI integration test
|
||||
uses: ./.github/workflows/_integration_test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -109,6 +184,8 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
test-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -139,6 +216,8 @@ jobs:
|
||||
test,
|
||||
test-langgraph,
|
||||
test-scheduler-kafka,
|
||||
check-sdk-methods,
|
||||
check-schema,
|
||||
integration-test,
|
||||
test-js,
|
||||
]
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: (Check for spelling errors)
|
||||
@@ -21,18 +25,18 @@
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install toml codespell jupytext
|
||||
pip install toml codespell==2.3.0 jupytext
|
||||
|
||||
- name: Extract Ignore Words List
|
||||
run: |
|
||||
# Use a Python script to extract the ignore words list from pyproject.toml
|
||||
python .github/workflows/extract_ignored_words_list.py
|
||||
python ../.github/workflows/extract_ignored_words_list.py
|
||||
id: extract_ignore_words
|
||||
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib'
|
||||
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.md'
|
||||
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
|
||||
# We do this to avoid spellchecking cell outputs
|
||||
- name: Codespell Notebooks
|
||||
|
||||
@@ -21,6 +21,10 @@ concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,6 +48,7 @@ jobs:
|
||||
deploy:
|
||||
# needs: run-changed-notebooks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||
steps:
|
||||
@@ -58,26 +63,57 @@ jobs:
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
cache-key: docs
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: docs/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with test --no-root
|
||||
yarn
|
||||
poetry install --with test --with docs --no-root
|
||||
poetry run pip install -U \
|
||||
pytest \
|
||||
pytest-check-links \
|
||||
langsmith \
|
||||
langchain \
|
||||
GitPython \
|
||||
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
|
||||
|
||||
# we run this installation only for internal PRs
|
||||
# as GITHUB_TOKEN is not available for PRs from outside contributors
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then
|
||||
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
fi
|
||||
|
||||
poetry run jupyter kernelspec list
|
||||
poetry run python3 -m ipykernel install --user --name=python3
|
||||
npm install -g tslab
|
||||
poetry run tslab install --python=python3
|
||||
poetry run jupyter kernelspec list
|
||||
|
||||
- name: Run unit tests
|
||||
# Run unit tests on the docs build pipeline
|
||||
run: make tests
|
||||
- name: Lint Docs
|
||||
# This step lints the docs using the existing linting set up.
|
||||
# It should be very fast and should not require any external services.
|
||||
run: make lint-docs
|
||||
- name: Build llms-text
|
||||
run: make llms-text
|
||||
- name: Build site
|
||||
run: make build-docs
|
||||
run: |
|
||||
# If this is main branch, then we want to download stats. we do this
|
||||
# with the env variable DOWNLOAD_STATS=true
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
DOWNLOAD_STATS=true make build-docs
|
||||
else
|
||||
make build-docs
|
||||
fi
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
|
||||
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
||||
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
||||
- name: Check links in notebooks
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
@@ -88,6 +124,7 @@ jobs:
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
@@ -95,14 +132,16 @@ jobs:
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
echo "Fetching changes from origin/main..."
|
||||
git fetch origin main
|
||||
echo "Checking for changed notebook files..."
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|docs/site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
if [ -n "${CHANGED_FILES}" ]; then
|
||||
echo "Running link check on HTML files matching changed notebook files..."
|
||||
@@ -113,8 +152,10 @@ jobs:
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links ${CHANGED_FILES} \
|
||||
|| ([ $? = 5 ] && exit 0 || exit $?)
|
||||
else
|
||||
@@ -127,7 +168,7 @@ jobs:
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/site/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import toml
|
||||
|
||||
pyproject_toml = toml.load("libs/langgraph/pyproject.toml")
|
||||
pyproject_toml = toml.load("pyproject.toml")
|
||||
|
||||
# Extract the ignore words list (adjust the key as per your TOML structure)
|
||||
ignore_words_list = (
|
||||
|
||||
@@ -195,7 +195,11 @@ jobs:
|
||||
"$PKG_NAME==$VERSION" \
|
||||
)
|
||||
|
||||
if [[ "$PKG_NAME" == *checkpoint* ]]; then
|
||||
if [[ "$PKG_NAME" == *prebuilt* ]]; then
|
||||
poetry run pip install langgraph
|
||||
fi
|
||||
|
||||
if [[ "$PKG_NAME" == *checkpoint* || "$PKG_NAME" == *prebuilt* ]]; then
|
||||
# since checkpoint packages are namespace packages, import them with . convention
|
||||
# i.e. import langgraph.checkpoint or langgraph.checkpoint.sqlite
|
||||
IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/./g)"
|
||||
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
schedule:
|
||||
- cron: '0 13 * * *'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -39,36 +43,36 @@ jobs:
|
||||
|
||||
- name: Pre-download tiktoken files
|
||||
run: |
|
||||
poetry run python docs/_scripts/download_tiktoken.py
|
||||
poetry run python _scripts/download_tiktoken.py
|
||||
|
||||
- name: Prepare notebooks
|
||||
run: |
|
||||
if [ "${{ matrix.lib-version }}" = "development" ]; then
|
||||
poetry run python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
poetry run python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
else
|
||||
poetry run python docs/_scripts/prepare_notebooks_for_ci.py
|
||||
poetry run python _scripts/prepare_notebooks_for_ci.py
|
||||
fi
|
||||
|
||||
- name: Run notebooks
|
||||
env:
|
||||
# these won't actually be used because of the VCR cassettes
|
||||
# but need to set them to avoid triggering getpass()
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
|
||||
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
OPENAI_API_KEY: "very-secret-key"
|
||||
ANTHROPIC_API_KEY: "very-secret-key"
|
||||
TAVILY_API_KEY: "very-secret-key"
|
||||
LANGSMITH_API_KEY: "very-secret-key"
|
||||
NOMIC_API_KEY: "very-secret-key"
|
||||
COHERE_API_KEY: "very-secret-key"
|
||||
FIREWORKS_API_KEY: "very-secret-key"
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "Running all notebooks"
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
./_scripts/execute_notebooks.sh
|
||||
else
|
||||
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | grep '\.ipynb$' || true)
|
||||
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | sed 's|^docs/docs/|docs/|' | grep '\.ipynb$' || true)
|
||||
if [ -n "$CHANGED_FILES" ]; then
|
||||
echo "Running changed notebooks: $CHANGED_FILES"
|
||||
./docs/_scripts/execute_notebooks.sh $CHANGED_FILES
|
||||
./_scripts/execute_notebooks.sh $CHANGED_FILES
|
||||
else
|
||||
echo "No notebook files changed, skipping execution"
|
||||
fi
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Check File Size
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
file-size-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
- name: Filter by size
|
||||
# TODO: roll back the web voyager hack
|
||||
run: |
|
||||
large_added_files=$(find ${{ steps.changed-files.outputs.added_files }} -maxdepth 0 -size +1M | grep -v "web_voyager" || true)
|
||||
if [ -n "$large_added_files" ]; then
|
||||
echo "Large files added: $large_added_files"
|
||||
echo "# Large files added:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$large_added_files" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
+2
-1
@@ -178,4 +178,5 @@ Untitled*.ipynb
|
||||
|
||||
Chinook.db
|
||||
|
||||
libs/langgraph/out
|
||||
.vercel
|
||||
.turbo
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc
|
||||
|
||||
build-typedoc:
|
||||
cd libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-docs: build-typedoc
|
||||
poetry run python -m mkdocs build --clean -f docs/mkdocs.yml --strict
|
||||
|
||||
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 -w ./libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs/docs -name "*.ipynb" -type f -delete
|
||||
rm -rf docs/site
|
||||
|
||||
## Run format against the project documentation.
|
||||
format-docs:
|
||||
poetry run ruff format docs/docs
|
||||
poetry run ruff check --fix docs/docs
|
||||
|
||||
# Check the docs for linting violations
|
||||
lint-docs:
|
||||
poetry run ruff format --check docs/docs
|
||||
poetry run ruff check docs/docs
|
||||
|
||||
codespell:
|
||||
./docs/codespell_notebooks.sh .
|
||||
|
||||
start-services:
|
||||
docker compose -f docs/test-compose.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f docs/test-compose.yml down
|
||||
@@ -1,288 +1,87 @@
|
||||
# 🦜🕸️LangGraph
|
||||
<picture class="github-only">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
|
||||
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
|
||||
</picture>
|
||||
|
||||

|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
⚡ Building language agents as graphs ⚡
|
||||
|
||||
> [!NOTE]
|
||||
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Overview
|
||||
LangGraph — used by Replit, Uber, LinkedIn, GitLab and more — is a low-level orchestration framework for building controllable agents. While langchain provides integrations and composable components to streamline LLM application development, the LangGraph library enables agent orchestration — offering customizable architectures, long-term memory, and human-in-the-loop to reliably handle complex tasks.
|
||||
|
||||
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
|
||||
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
|
||||
|
||||
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Cycles and Branching**: Implement loops and conditionals in your apps.
|
||||
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
|
||||
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
|
||||
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
|
||||
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
|
||||
|
||||
### LangGraph Platform
|
||||
|
||||
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
|
||||
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
|
||||
|
||||
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
|
||||
- **Background runs**: Runs agents asynchronously in the background
|
||||
- **Support for long running agents**: Infrastructure that can handle long running processes
|
||||
- **[Double texting](https://langchain-ai.github.io/langgraph/concepts/double_texting)**: Handle the case where you get two messages from the user before the agent can respond
|
||||
- **Handle burstiness**: Task queue for ensuring requests are handled consistently without loss, even under heavy loads
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
```
|
||||
|
||||
```shell
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
```
|
||||
|
||||
Optionally, we can set up [LangSmith](https://docs.smith.langchain.com/) for best-in-class observability.
|
||||
|
||||
```shell
|
||||
export LANGSMITH_TRACING=true
|
||||
export LANGSMITH_API_KEY=lsv2_sk_...
|
||||
```
|
||||
|
||||
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
|
||||
|
||||
<details open>
|
||||
<summary>High-level implementation</summary>
|
||||
To learn more about how to use LangGraph, check out [the docs](https://langchain-ai.github.io/langgraph/). We show a simple example below of how to create a ReAct agent.
|
||||
|
||||
```python
|
||||
# This code depends on pip install langchain[anthropic]
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
app = create_react_agent(model, tools, checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
agent = create_react_agent("anthropic:claude-3-7-sonnet-latest", tools=[search])
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!TIP]
|
||||
> LangGraph is a **low-level** framework that allows you to implement any custom agent
|
||||
architectures. Click on the low-level implementation below to see how to implement a
|
||||
tool-calling agent from scratch.
|
||||
|
||||
<details>
|
||||
<summary>Low-level implementation</summary>
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import END, START, StateGraph, MessagesState
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: MessagesState) -> Literal["tools", END]:
|
||||
messages = state['messages']
|
||||
last_message = messages[-1]
|
||||
# If the LLM makes a tool call, then we route to the "tools" node
|
||||
if last_message.tool_calls:
|
||||
return "tools"
|
||||
# Otherwise, we stop (reply to the user)
|
||||
return END
|
||||
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state: MessagesState):
|
||||
messages = state['messages']
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(MessagesState)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("tools", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("tools", 'agent')
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable.
|
||||
# Note that we're (optionally) passing the memory when compiling the graph
|
||||
app = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
</details>
|
||||
|
||||
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
|
||||
```
|
||||
## Why use LangGraph?
|
||||
|
||||
### Step-by-step Breakdown
|
||||
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
|
||||
|
||||
1. <details>
|
||||
<summary>Initialize the model and tools.</summary>
|
||||
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
|
||||
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
|
||||
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
|
||||
|
||||
- we use `ChatAnthropic` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
|
||||
- we define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
|
||||
</details>
|
||||
LangGraph is trusted in production and powering agents for companies like:
|
||||
|
||||
2. <details>
|
||||
<summary>Initialize graph with state.</summary>
|
||||
- [Klarna](https://blog.langchain.dev/customers-klarna/): Customer support bot for 85 million active users
|
||||
- [Elastic](https://www.elastic.co/blog/elastic-security-generative-ai-features): Security AI assistant for threat detection
|
||||
- [Uber](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/): Automated unit test generation
|
||||
- [Replit](https://www.langchain.com/breakoutagents/replit): Code generation
|
||||
- And many more ([see list here](https://www.langchain.com/built-with-langgraph))
|
||||
|
||||
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
|
||||
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
|
||||
</details>
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
3. <details>
|
||||
<summary>Define graph nodes.</summary>
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
There are two main nodes we need:
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
|
||||
- The `agent` node: responsible for deciding what (if any) actions to take.
|
||||
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
|
||||
</details>
|
||||
## Pairing with LangGraph Platform
|
||||
|
||||
4. <details>
|
||||
<summary>Define entry point and graph edges.</summary>
|
||||
While LangGraph is our open-source agent orchestration framework, enterprises that need scalable agent deployment can benefit from [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/).
|
||||
|
||||
First, we need to set the entry point for graph execution - `agent` node.
|
||||
LangGraph Platform can help engineering teams:
|
||||
|
||||
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
|
||||
- **Accelerate agent development**: Quickly create agent UXs with configurable templates and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/) for visualizing and debugging agent interactions.
|
||||
- **Deploy seamlessly**: We handle the complexity of deploying your agent. LangGraph Platform includes robust APIs for memory, threads, and cron jobs plus auto-scaling task queues & servers.
|
||||
- **Centralize agent management & reusability**: Discover, reuse, and manage agents across the organization. Business users can also modify agents without coding.
|
||||
|
||||
- Conditional edge: after the agent is called, we should either:
|
||||
- a. Run tools if the agent said to take an action, OR
|
||||
- b. Finish (respond to the user) if the agent did not ask to run tools
|
||||
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
|
||||
</details>
|
||||
## Additional resources
|
||||
|
||||
5. <details>
|
||||
<summary>Compile the graph.</summary>
|
||||
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
|
||||
- [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Simple walkthroughs with guided examples on getting started with LangGraph.
|
||||
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
|
||||
- [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
|
||||
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
|
||||
</details>
|
||||
## Acknowledgements
|
||||
|
||||
6. <details>
|
||||
<summary>Execute the graph.</summary>
|
||||
|
||||
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
|
||||
2. The `"agent"` node executes, invoking the chat model.
|
||||
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
|
||||
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
|
||||
|
||||
- If `AIMessage` has `tool_calls`, `"tools"` node executes
|
||||
- The `"agent"` node executes again and returns `AIMessage`
|
||||
|
||||
5. Execution progresses to the special `END` value and outputs the final state.
|
||||
And as a result, we get a list of all our chat messages as output.
|
||||
</details>
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
|
||||
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
|
||||
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
|
||||
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
|
||||
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
|
||||
|
||||
## Contributing
|
||||
|
||||
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
@@ -1,2 +1,4 @@
|
||||
site/
|
||||
docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
|
||||
|
||||
build-typedoc:
|
||||
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-prebuilt:
|
||||
# Use to create an update to date prebuilt page.
|
||||
# Looks up download stats for each of the prebuilt packages and
|
||||
# generates the final prebuilt page.
|
||||
@if [ "$(DOWNLOAD_STATS)" = "true" ]; then \
|
||||
set -x; \
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml; \
|
||||
set +x; \
|
||||
else \
|
||||
set -x; \
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats --fake stats.yml; \
|
||||
set +x; \
|
||||
fi
|
||||
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
poetry run python -m _scripts.generate_llms_text docs/llms-full.txt
|
||||
|
||||
install-vercel-deps:
|
||||
dnf install -y python3.11
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry self update 1.8.5
|
||||
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
|
||||
poetry env use /usr/bin/python3.11
|
||||
poetry install --with docs --with test --no-root
|
||||
|
||||
tests:
|
||||
# Run unit tests
|
||||
poetry run pytest tests/unit_tests
|
||||
|
||||
|
||||
vercel-build-docs: install-vercel-deps
|
||||
make build-docs
|
||||
|
||||
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs -name "*.ipynb" -type f -delete
|
||||
rm -rf site
|
||||
|
||||
## Run format against the project documentation.
|
||||
format-docs:
|
||||
poetry run ruff format docs
|
||||
poetry run ruff check --fix docs
|
||||
|
||||
# Check the docs for linting violations
|
||||
lint-docs:
|
||||
poetry run ruff format --check docs
|
||||
poetry run ruff check docs
|
||||
|
||||
codespell:
|
||||
./codespell_notebooks.sh .
|
||||
|
||||
start-services:
|
||||
docker compose -f test-compose.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f test-compose.yml down
|
||||
+9
-9
@@ -14,28 +14,28 @@ To run the documentation server locally you can run:
|
||||
make serve-docs
|
||||
```
|
||||
|
||||
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
|
||||
|
||||
## Execute notebooks
|
||||
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||
|
||||
```bash
|
||||
python docs/_scripts/prepare_notebooks_for_ci.py
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
python _scripts/prepare_notebooks_for_ci.py
|
||||
./_scripts/execute_notebooks.sh
|
||||
```
|
||||
|
||||
**Note**: if you want to run the notebooks without `%pip install` cells, you can run:
|
||||
|
||||
```bash
|
||||
python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
./_scripts/execute_notebooks.sh
|
||||
```
|
||||
|
||||
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
|
||||
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
||||
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
||||
|
||||
**Note**: this is currently limited only to the notebooks in `docs/docs/how-tos`
|
||||
|
||||
## Adding new notebooks
|
||||
|
||||
If you are adding a notebook with API requests, it's **recommended** to record network requests so that they can be subsequently replayed. If this is not done, the notebook runner will make API requests every time the notebook is run, which can be costly and slow.
|
||||
@@ -48,14 +48,14 @@ Then, run
|
||||
jupyter execute <path_to_notebook>
|
||||
```
|
||||
|
||||
Once the notebook is executed, you should see the new VCR cassettes recorded in `docs/cassettes` directory and discard the updated notebook.
|
||||
Once the notebook is executed, you should see the new VCR cassettes recorded in `cassettes` directory and discard the updated notebook.
|
||||
|
||||
## Updating existing notebooks
|
||||
|
||||
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `docs/cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
|
||||
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
|
||||
|
||||
To delete cassettes for a notebook, you can run:
|
||||
|
||||
```bash
|
||||
rm docs/cassettes/<notebook_name>*
|
||||
rm cassettes/<notebook_name>*
|
||||
```
|
||||
@@ -31,7 +31,7 @@ def request(self, method, url, body=None, headers=None):
|
||||
The result of calling the parent request method.
|
||||
"""
|
||||
# Update the inner socket's timeout value to send the request.
|
||||
# This only triggers if the connection is re-used.
|
||||
# This only triggers if the connection is reused.
|
||||
if getattr(self, "sock", None) is not None:
|
||||
self.sock.settimeout(self.timeout)
|
||||
|
||||
@@ -90,4 +90,4 @@ def patch_urllib3():
|
||||
return request(self, *args, **kwargs)
|
||||
|
||||
connection.HTTPConnection.request = new_request
|
||||
_PATCHED = True
|
||||
_PATCHED = True
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Add typescript translation to a given markdown file."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
|
||||
import requests
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
|
||||
response = requests.get(URL)
|
||||
response.raise_for_status()
|
||||
reference_snippets = response.text
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
|
||||
|
||||
def _get_tqdm():
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
# If not available return a simple identity function
|
||||
def tqdm(iterable, *args, **kwargs):
|
||||
return iterable
|
||||
|
||||
return tqdm
|
||||
|
||||
|
||||
_tqdm = _get_tqdm()
|
||||
|
||||
opening_pattern = re.compile(r"^\s*```python(?:\s+.*)?\s*$")
|
||||
closing_pattern = re.compile(r"^\s*```\s*$")
|
||||
|
||||
|
||||
def extract_python_snippets(markdown: str) -> list[str]:
|
||||
"""
|
||||
Extract all python code blocks (including their fence lines) from the markdown content.
|
||||
A python block is defined as any block that starts with a line containing an opening fence
|
||||
with '```python' (optionally with extra parameters) and ends with a closing fence '```'.
|
||||
"""
|
||||
snippets = []
|
||||
inside_block = False
|
||||
current_snippet = []
|
||||
|
||||
for line in markdown.splitlines(keepends=True):
|
||||
if not inside_block:
|
||||
if opening_pattern.match(line):
|
||||
inside_block = True
|
||||
current_snippet = [line]
|
||||
else:
|
||||
current_snippet.append(line)
|
||||
if closing_pattern.match(line):
|
||||
inside_block = False
|
||||
snippets.append("".join(current_snippet))
|
||||
current_snippet = []
|
||||
return snippets
|
||||
|
||||
|
||||
def translate_snippet(python_snippet: str) -> str:
|
||||
"""Translate a python code block into a TypeScript code block using Langchain.
|
||||
The response is expected to be a properly fenced TypeScript code block (i.e.
|
||||
starting with ```typescript and ending with ```).
|
||||
"""
|
||||
ai_message = model.invoke(
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You have access to the following up-to-date example TypeScript code "
|
||||
f"snippets that show examples of building with langgraph "
|
||||
f"and langchain:\n\n{reference_snippets}\n\n"
|
||||
"Use this context to translate the following Python code to equivalent "
|
||||
"TypeScript. Ensure that your output is a valid fenced TypeScript "
|
||||
"code block (i.e. starts with ```typescript and ends with ```)."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Translate this Python snippet to TypeScript:\n\n{python_snippet}",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Use a regular expression to search for a TypeScript code block in the response.
|
||||
pattern = r"```typescript\s*(.*?)\s*```"
|
||||
match = re.search(pattern, ai_message.content, re.DOTALL)
|
||||
if match:
|
||||
# Reconstruct the code block with proper fences.
|
||||
typescript_code = match.group(1).strip()
|
||||
return f"```typescript\n{typescript_code}\n```"
|
||||
else:
|
||||
raise ValueError("No TypeScript code block found in the model's response.")
|
||||
|
||||
|
||||
def insert_translations_into_markdown(
|
||||
markdown: str, typescript_snippets: list[str]
|
||||
) -> str:
|
||||
"""Walks through the original markdown content and, after each
|
||||
Python snippet block, inserts the corresponding translated TypeScript snippet.
|
||||
It assumes that the ordering of the Python snippets
|
||||
(from extract_python_snippets) matches the order they appear in the markdown.
|
||||
"""
|
||||
output_lines = []
|
||||
lines = markdown.splitlines(keepends=True)
|
||||
inside_block = False
|
||||
snippet_index = 0
|
||||
|
||||
for line in lines:
|
||||
output_lines.append(line)
|
||||
if not inside_block and opening_pattern.match(line):
|
||||
# We've encountered the start of a python code block.
|
||||
inside_block = True
|
||||
elif inside_block:
|
||||
if closing_pattern.match(line):
|
||||
# End of a python snippet block.
|
||||
inside_block = False
|
||||
if snippet_index < len(typescript_snippets):
|
||||
# Insert an extra newline for clarity, then the translated TypeScript snippet.
|
||||
output_lines.append("\n")
|
||||
output_lines.append(typescript_snippets[snippet_index])
|
||||
output_lines.append("\n")
|
||||
snippet_index += 1
|
||||
return "".join(output_lines)
|
||||
|
||||
|
||||
def main(file_path: str) -> None:
|
||||
# Read the markdown file.
|
||||
with open(file_path, "r") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
# 1. Extract all Python snippets.
|
||||
python_snippets = extract_python_snippets(markdown_content)[:1]
|
||||
|
||||
# 2. Translate each Python snippet to TypeScript.
|
||||
typescript_snippets = []
|
||||
# Replace with .batch() for faster translation
|
||||
for python_snippet in _tqdm(python_snippets):
|
||||
ts_snippet = translate_snippet(python_snippet)
|
||||
typescript_snippets.append(ts_snippet)
|
||||
|
||||
# 3. Insert the TypeScript translations after their respective Python snippets.
|
||||
updated_markdown = insert_translations_into_markdown(
|
||||
markdown_content, typescript_snippets
|
||||
)
|
||||
|
||||
# Overwrite the original markdown file with the updated content.
|
||||
with open(file_path, "w") as f:
|
||||
f.write(updated_markdown)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Translate Python snippets in a markdown file to TypeScript and insert them after each Python snippet."
|
||||
)
|
||||
parser.add_argument("file_path", type=str, help="Path to the markdown file.")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.file_path)
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read the list of notebooks to skip from the JSON file
|
||||
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('docs/notebooks_no_execution.json'))))")
|
||||
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('notebooks_no_execution.json'))))")
|
||||
|
||||
# Function to execute a single notebook
|
||||
execute_notebook() {
|
||||
@@ -27,7 +27,7 @@ if [ $# -gt 0 ]; then
|
||||
notebooks=$(echo "$@" | tr ' ' '\n' | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
else
|
||||
# Find all notebooks and filter out those in the skip list
|
||||
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
notebooks=$(find docs/tutorials docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
fi
|
||||
|
||||
# Execute notebooks sequentially
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import ast
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Literal, Optional
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional
|
||||
|
||||
import nbformat
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,14 +39,17 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
|
||||
([], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
|
||||
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
|
||||
(["langgraph.constants"], "langgraph.types", "Send", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Command", "types"),
|
||||
([], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
|
||||
(["langgraph.func"], "langgraph.func", "task", "func"),
|
||||
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
|
||||
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
|
||||
@@ -72,36 +69,19 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
|
||||
}
|
||||
|
||||
|
||||
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
|
||||
if not pkg_prefix.isidentifier():
|
||||
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
|
||||
return re.compile(
|
||||
r"from\s+(" + pkg_prefix + "(?:_\w+)?(?:\.\w+)*?)\s+import\s+"
|
||||
r"((?:\w+(?:,\s*)?)*" # Match zero or more words separated by a comma+optional ws
|
||||
r"(?:\s*\(.*?\))?)", # Match optional parentheses block
|
||||
re.DOTALL, # Match newlines as well
|
||||
)
|
||||
|
||||
|
||||
# Regular expression to match langchain import lines
|
||||
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
|
||||
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
|
||||
|
||||
|
||||
|
||||
|
||||
@lru_cache(maxsize=10_000)
|
||||
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
"""Get full module name using inspect, with LRU cache to memoize results."""
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
class_ = getattr(module, class_name)
|
||||
module = inspect.getmodule(class_)
|
||||
if module is None:
|
||||
# For constants, inspect.getmodule() might return None
|
||||
# In this case, we'll return the original module_path
|
||||
symbol = getattr(module, class_name)
|
||||
# First check the __module__ attribute on the symbol.
|
||||
mod_name = getattr(symbol, "__module__", None)
|
||||
# If __module__ is not set or comes from typing,
|
||||
# assume the definition is in module_path.
|
||||
if mod_name is None or mod_name.startswith("typing"):
|
||||
return module_path
|
||||
return module.__name__
|
||||
return mod_name
|
||||
except AttributeError as e:
|
||||
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
|
||||
return None
|
||||
@@ -109,139 +89,129 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
|
||||
return None
|
||||
|
||||
def _get_doc_title(data: str, file_name: str) -> str:
|
||||
try:
|
||||
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
pass
|
||||
# Parse the rst-style titles
|
||||
try:
|
||||
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
return file_name
|
||||
|
||||
|
||||
class ImportInformation(TypedDict):
|
||||
imported: str # The name of the class that was imported.
|
||||
source: str # The full module path from which the class was imported.
|
||||
docs: str # The URL pointing to the class's documentation.
|
||||
title: str # The title of the document where the import is used.
|
||||
path: str # The path of the file where the markdown content originated.
|
||||
|
||||
|
||||
def _get_imports(
|
||||
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
|
||||
) -> List[ImportInformation]:
|
||||
"""Get imports from the given code block.
|
||||
|
||||
Args:
|
||||
code: Python code block from which to extract imports
|
||||
doc_title: Title of the document
|
||||
package_ecosystem: "langchain" or "langgraph". The two live in different
|
||||
repositories and have separate documentation sites.
|
||||
|
||||
Returns:
|
||||
List of import information for the given code block
|
||||
"""
|
||||
imports = []
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pattern = _IMPORT_LANGCHAIN_RE
|
||||
elif package_ecosystem == "langgraph":
|
||||
pattern = _IMPORT_LANGGRAPH_RE
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
for import_match in pattern.finditer(code):
|
||||
module = import_match.group(1)
|
||||
if "pydantic_v1" in module:
|
||||
continue
|
||||
imports_str = (
|
||||
import_match.group(2).replace("(\n", "").replace("\n)", "")
|
||||
) # Handle newlines within parentheses
|
||||
# remove any newline and spaces, then split by comma
|
||||
imported_classes = [
|
||||
imp.strip()
|
||||
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
|
||||
if imp.strip()
|
||||
]
|
||||
for class_name in imported_classes:
|
||||
module_path = _get_full_module_name(module, class_name)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"title": doc_title,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
|
||||
def get_imports(code: str, path: str) -> List[ImportInformation]:
|
||||
"""Retrieve all import references from the given code for specified ecosystems.
|
||||
|
||||
Args:
|
||||
code: The source code from which to extract import references.
|
||||
doc_title: The documentation title associated with the code.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
A list of import information for each import found.
|
||||
"""
|
||||
ecosystems = ["langchain", "langgraph"]
|
||||
all_imports = []
|
||||
for package_ecosystem in ecosystems:
|
||||
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
|
||||
return all_imports
|
||||
# Parse the code into an AST.
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
found_imports = []
|
||||
|
||||
# Walk through the AST and process ImportFrom nodes.
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
# node.module is the source module.
|
||||
if node.module is None:
|
||||
continue
|
||||
for alias in node.names:
|
||||
if not (
|
||||
node.module.startswith("langchain")
|
||||
or node.module.startswith("langgraph")
|
||||
):
|
||||
continue
|
||||
|
||||
found_imports.append(
|
||||
{
|
||||
"source": node.module,
|
||||
# alias.name is the original name even if an alias exists.
|
||||
"imported": alias.name,
|
||||
}
|
||||
)
|
||||
|
||||
imports: list[ImportInformation] = []
|
||||
|
||||
for found_import in found_imports:
|
||||
module = found_import["source"]
|
||||
|
||||
if module.startswith("langchain"):
|
||||
# Handles things like `langchain` or `langchain_anthropic`
|
||||
package_ecosystem = "langchain"
|
||||
elif module.startswith("langgraph"):
|
||||
package_ecosystem = "langgraph"
|
||||
else:
|
||||
continue
|
||||
|
||||
class_name = found_import["imported"]
|
||||
module_path = _get_full_module_name(module, class_name)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"path": path,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def update_markdown_with_imports(markdown: str) -> str:
|
||||
def update_markdown_with_imports(markdown: str, path: str) -> str:
|
||||
"""Update markdown to include API reference links for imports in Python code blocks.
|
||||
|
||||
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
|
||||
This function scans the markdown content for Python code blocks, extracts any
|
||||
imports, and appends links to their API documentation.
|
||||
|
||||
Args:
|
||||
markdown: The markdown content to process.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
Updated markdown with API reference links appended to Python code blocks.
|
||||
@@ -252,10 +222,12 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
```python
|
||||
from langchain.nlp import TextGenerator
|
||||
```
|
||||
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
|
||||
This function will append an API reference link to the `TextGenerator` class
|
||||
from the `langchain.nlp` module if it's recognized.
|
||||
"""
|
||||
code_block_pattern = re.compile(
|
||||
r'(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```', re.DOTALL
|
||||
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def replace_code_block(match: re.Match) -> str:
|
||||
@@ -267,9 +239,8 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
Returns:
|
||||
str: The modified code block with API reference links appended if applicable.
|
||||
"""
|
||||
indent = match.group('indent')
|
||||
code_block = match.group('code')
|
||||
language = match.group('language') # Preserve the language from the regex match
|
||||
indent = match.group("indent")
|
||||
code_block = match.group("code")
|
||||
# Retrieve import information from the code block
|
||||
imports = get_imports(code_block, "__unused__")
|
||||
|
||||
@@ -279,12 +250,12 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
return original_code_block
|
||||
|
||||
# Generate API reference links for each import
|
||||
api_links = ' | '.join(
|
||||
api_links = " | ".join(
|
||||
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
|
||||
)
|
||||
# Return the code block with appended API reference links
|
||||
return f'{original_code_block}\n\n{indent}API Reference: {api_links}'
|
||||
return f"{original_code_block}\n\n{indent}API Reference: {api_links}"
|
||||
|
||||
# Apply the replace_code_block function to all matches in the markdown
|
||||
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
|
||||
return updated_markdown
|
||||
return updated_markdown
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Experimental script to generate consolidated llms text from the docs."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from _scripts.notebook_hooks import _on_page_markdown_with_config
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get source directory (parent of HERE / docs)
|
||||
SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(HERE), "docs"))
|
||||
|
||||
|
||||
def _make_llms_text(output_file: str) -> str:
|
||||
"""Generate a consolidated text file from markdown/notebook files for LLM training.
|
||||
|
||||
Args:
|
||||
output_file: Path to output the consolidated text file
|
||||
"""
|
||||
# Collect all markdown and notebook files
|
||||
relative_paths = [
|
||||
# Files relative to docs/docs/
|
||||
"tutorials/introduction.ipynb",
|
||||
]
|
||||
all_files = [os.path.join(SOURCE_DIR, path) for path in relative_paths]
|
||||
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.ipynb"), recursive=True)
|
||||
)
|
||||
# Add all concepts
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.ipynb"), recursive=True)
|
||||
)
|
||||
|
||||
all_content = []
|
||||
|
||||
# Process each file
|
||||
for file_path in all_files:
|
||||
print(f"Processing {file_path}")
|
||||
rel_path = os.path.relpath(file_path, SOURCE_DIR)
|
||||
|
||||
# Create File and Page objects to match mkdocs structure
|
||||
file_obj = File(
|
||||
path=rel_path, src_dir=SOURCE_DIR, dest_dir="", use_directory_urls=True
|
||||
)
|
||||
page = Page(
|
||||
title="",
|
||||
file=file_obj,
|
||||
config={},
|
||||
)
|
||||
|
||||
# Read raw content
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Convert to markdown without logic to resolve API references
|
||||
processed_content = _on_page_markdown_with_config(
|
||||
content, page, add_api_references=False, remove_base64_images=True
|
||||
)
|
||||
if processed_content:
|
||||
# Add file name
|
||||
all_content.append(f"---\n{rel_path}\n---")
|
||||
# Add content
|
||||
all_content.append(processed_content)
|
||||
|
||||
# Write consolidated output
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write("\n\n".join(all_content))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Generate consolidated text file from markdown/notebook files for LLMs."
|
||||
)
|
||||
)
|
||||
parser.add_argument("output_file", help="Path to output the consolidated text file")
|
||||
|
||||
args = parser.parse_args()
|
||||
_make_llms_text(args.output_file)
|
||||
@@ -1,28 +1,266 @@
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
|
||||
def _uses_input(source: str) -> bool:
|
||||
"""Parse the source code to determine if it uses the input() function."""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
# If there's a syntax error, assume input() might be present to be safe.
|
||||
return False
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call):
|
||||
# Check if the function called is named 'input'
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "input":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _rewrite_cell_magic(code: str) -> str:
|
||||
"""Process a code block that uses cell magic.:w
|
||||
|
||||
- Lines starting with "%%capture" are ignored.
|
||||
- Lines starting with "%pip" are rewritten by removing the leading "%" character.
|
||||
- Any other non-empty line causes a NotImplementedError.
|
||||
|
||||
Args:
|
||||
code (str): The original code block.
|
||||
|
||||
Returns:
|
||||
str: The transformed code block.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If a line doesn't start with either "%%capture" or "%pip".
|
||||
"""
|
||||
rewritten_lines = []
|
||||
|
||||
for line in code.splitlines():
|
||||
stripped = line.strip()
|
||||
# Skip empty lines
|
||||
if not stripped:
|
||||
continue
|
||||
# Ignore %%capture lines
|
||||
if stripped.startswith("%%capture"):
|
||||
continue
|
||||
# Rewrite %pip lines by dropping the '%'
|
||||
elif stripped.startswith("%pip"):
|
||||
# Drop the leading '%' character
|
||||
rewritten_lines.append(stripped[1:])
|
||||
# Anything else is not supported
|
||||
else:
|
||||
raise NotImplementedError(f"Unhandled line: {line}")
|
||||
|
||||
return "\n".join(rewritten_lines)
|
||||
|
||||
|
||||
class PrintCallVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
This visitor sets self.has_print to True if it encounters a call
|
||||
to a print within the global scope.
|
||||
|
||||
This should catch calls to print(), print_stream(), etc. (Prefixed with "print").
|
||||
|
||||
May have some false positives, but it's not meant to be perfect.
|
||||
|
||||
Temporary code for notebook conversion.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.has_print = False
|
||||
self.scope_level = 0 # counter to track whether we're inside a def/lambda
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Lambda(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Call(self, node):
|
||||
# Only consider calls when not inside a function definition.
|
||||
if self.scope_level == 0:
|
||||
if isinstance(node.func, ast.Name) and node.func.id.startswith("print"):
|
||||
self.has_print = True
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def _has_output(source: str) -> bool:
|
||||
"""Determine if the code block is expected to produce output.
|
||||
|
||||
Args:
|
||||
source (str): The source code of the code block.
|
||||
|
||||
Returns:
|
||||
True if the code block is expected to produce output, False otherwise.
|
||||
|
||||
Must meet the following conditions:
|
||||
|
||||
1. There is a call to a printing function (name starts with "print")
|
||||
that is not inside a function definition.
|
||||
2. The last top-level statement is an expression that is valid if:
|
||||
- It is any expression (including calls) AND
|
||||
- It is NOT a call to `display(...)`.
|
||||
|
||||
`display` isn't handled currently by markdown-exec
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return False
|
||||
|
||||
# Condition (1): Check for a global print-like call.
|
||||
visitor = PrintCallVisitor()
|
||||
visitor.visit(tree)
|
||||
condition_a = visitor.has_print
|
||||
|
||||
# Condition (2): Check the last top-level statement.
|
||||
condition_b = False
|
||||
if tree.body:
|
||||
last_stmt = tree.body[-1]
|
||||
if isinstance(last_stmt, ast.Expr):
|
||||
# If the expression is a call, ensure it's not a call to "display"
|
||||
if isinstance(last_stmt.value, ast.Call):
|
||||
if (
|
||||
isinstance(last_stmt.value.func, ast.Name)
|
||||
and last_stmt.value.func.id == "display"
|
||||
):
|
||||
condition_b = False # exclude display-wrapped expressions
|
||||
else:
|
||||
condition_b = True
|
||||
else:
|
||||
# Any other expression qualifies.
|
||||
condition_b = True
|
||||
|
||||
return condition_a or condition_b
|
||||
|
||||
|
||||
def _convert_links_in_markdown(markdown: str) -> str:
|
||||
"""Convert links present in notebook markdown cells to standardized format.
|
||||
|
||||
We want to update markdown links code cells by linking to markdown
|
||||
files rather than assuming that the link is to the finalized HTML.
|
||||
|
||||
This code is needed temporarily since the markdown links that are present
|
||||
in ipython notebooks do not follow the same conventions as regular markdown
|
||||
files in mkdocs (which should link to a .md file).
|
||||
"""
|
||||
|
||||
# Define the regex pattern in parts for clarity:
|
||||
pattern = (
|
||||
r"(?<!!)" # Negative lookbehind: ensure the link is not an image (i.e., doesn't start with "!")
|
||||
r"\[" # Literal '[' indicating the start of the link text.
|
||||
r"(?P<text>[^\]]*)" # Named group 'text': match any characters except ']', representing the link text.
|
||||
r"\]" # Literal ']' indicating the end of the link text.
|
||||
r"\(" # Literal '(' indicating the start of the URL.
|
||||
r"(?![^\)]*//)" # Negative lookahead: ensure that the URL does not contain '//' (skip absolute URLs).
|
||||
r"(?P<url>[^)]*)" # Named group 'url': match any characters except ')', representing the URL.
|
||||
r"\)" # Literal ')' indicating the end of the URL.
|
||||
)
|
||||
|
||||
def custom_replacement(match):
|
||||
"""logic will correct the link format used in ipython notebooks
|
||||
|
||||
Ipython notebooks were being converted directly into HTML links
|
||||
instead of markdown links that retain the markdown extension.
|
||||
|
||||
It needs to handle the following cases:
|
||||
- optional fragments (e.g., `#section`)
|
||||
e.g., `[text](url/#section)` -> `[text](url.md#section)`
|
||||
e.g., `[text](url#section)` -> `[text](url.md#section)`
|
||||
- relative paths (e.g., `../path/to/file`) need to be denested by 1 level
|
||||
"""
|
||||
text = match.group("text")
|
||||
url = match.group("url")
|
||||
|
||||
if url.startswith("../"):
|
||||
# we strip the "../" from the start of the URL
|
||||
# We only need to denest one level.
|
||||
url = url[3:]
|
||||
|
||||
url = url.rstrip("/") # Strip `/` from the end of the URL
|
||||
|
||||
# if url has a fragment
|
||||
if "#" in url:
|
||||
url, fragment = url.split("#")
|
||||
url = url.rstrip("/")
|
||||
# Strip `/` from the end of the URL
|
||||
return f"[{text}]({url}.md#{fragment})"
|
||||
# Otherwise add the .md extension
|
||||
return f"[{text}]({url}.md)"
|
||||
|
||||
return re.sub(
|
||||
pattern,
|
||||
custom_replacement,
|
||||
markdown,
|
||||
)
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.markdown_exec_migration = markdown_exec_migration
|
||||
|
||||
def preprocess_cell(self, cell, resources, cell_index):
|
||||
if cell.cell_type == "markdown":
|
||||
# rewrite markdown links to html links (excluding image links)
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
if not self.markdown_exec_migration:
|
||||
# Old logic is to convert ipynb links to HTML links
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
else:
|
||||
cell.source = _convert_links_in_markdown(cell.source)
|
||||
|
||||
# Fix image paths in <img> tags
|
||||
cell.source = re.sub(
|
||||
r'<img\s+src="\.?/img/([^"]+)"', r'<img src="../img/\1"', cell.source
|
||||
)
|
||||
|
||||
elif cell.cell_type == "code":
|
||||
# Determine if the cell has bash or cell magic
|
||||
source = cell.source
|
||||
is_exec = not (
|
||||
source.startswith("%") or source.startswith("!") or _uses_input(source)
|
||||
)
|
||||
cell.metadata["exec"] = is_exec
|
||||
|
||||
if self.markdown_exec_migration:
|
||||
# For markdown exec migration we'll re-write cell magic as bash commands
|
||||
if source.startswith("%%"):
|
||||
cell.source = _rewrite_cell_magic(source)
|
||||
cell.metadata["language"] = "shell"
|
||||
|
||||
cell.metadata["has_output"] = _has_output(source)
|
||||
|
||||
# Remove noqa comments
|
||||
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
|
||||
# escape ``` in code
|
||||
# This is needed because the markdown exporter will wrap code blocks in
|
||||
# triple backticks, which will break the markdown output if the code block
|
||||
# contains triple backticks.
|
||||
cell.source = cell.source.replace("```", r"\`\`\`")
|
||||
# escape ``` in output
|
||||
if "outputs" in cell:
|
||||
@@ -115,9 +353,11 @@ exporter = MarkdownExporter(
|
||||
|
||||
def convert_notebook(
|
||||
notebook_path: Path,
|
||||
) -> Path:
|
||||
mode: Literal["markdown", "exec"] = "markdown",
|
||||
) -> str:
|
||||
with open(notebook_path) as f:
|
||||
nb = nbformat.read(f, as_version=4)
|
||||
|
||||
nb.metadata.mode = mode
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
return body
|
||||
|
||||
+154
-19
@@ -1,13 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from notebook_convert import convert_notebook
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -15,6 +16,24 @@ logger.setLevel(logging.INFO)
|
||||
DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
||||
|
||||
|
||||
REDIRECT_MAP = {
|
||||
# lib redirects
|
||||
"how-tos/stream-values.ipynb": "how-tos/streaming.ipynb#values",
|
||||
"how-tos/stream-updates.ipynb": "how-tos/streaming.ipynb#updates",
|
||||
"how-tos/streaming-content.ipynb": "how-tos/streaming.ipynb#custom",
|
||||
"how-tos/stream-multiple.ipynb": "how-tos/streaming.ipynb#multiple",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming-tokens.ipynb#example-without-langchain",
|
||||
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "concepts/index.md#langgraph-platform",
|
||||
"cloud/how-tos/index.md": "how-tos/index.md#langgraph-platform",
|
||||
"cloud/concepts/api.md": "concepts/langgraph_server.md",
|
||||
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
|
||||
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
|
||||
}
|
||||
|
||||
|
||||
class NotebookFile(File):
|
||||
def is_documentation_page(self):
|
||||
return True
|
||||
@@ -38,6 +57,29 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
|
||||
return new_files
|
||||
|
||||
|
||||
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
|
||||
"""Add the path to the code blocks."""
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
|
||||
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
|
||||
r"(?P=indent)```" # Match closing backticks with the same indentation
|
||||
)
|
||||
|
||||
def replace_code_block_header(match: re.Match) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
if 'exec="on"' not in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
code = match.group("code")
|
||||
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
|
||||
|
||||
return code_block_pattern.sub(replace_code_block_header, markdown)
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
@@ -52,7 +94,7 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# existing hl_lines for Python and JavaScript
|
||||
# Pattern to find code blocks with highlight comments, handling optional indentation
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
|
||||
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
|
||||
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
|
||||
r"(?P=indent)```" # Match closing backticks with the same indentation
|
||||
)
|
||||
@@ -61,6 +103,13 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
code_block = match.group("code")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
# Account for a case where hl_lines is manually specified
|
||||
if "hl_lines" in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
lines = code_block.split("\n")
|
||||
highlighted_lines = []
|
||||
|
||||
@@ -86,35 +135,121 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# Reconstruct the new code block
|
||||
new_code_block = "\n".join(lines_to_keep)
|
||||
|
||||
# Construct the full code block that also includes
|
||||
# the fenced code block syntax.
|
||||
opening_fence = f"```{language}"
|
||||
|
||||
if attributes:
|
||||
opening_fence += f" {attributes}"
|
||||
|
||||
if highlighted_lines:
|
||||
return (
|
||||
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f'{new_code_block}'
|
||||
f'{indent}```'
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"{indent}```{language}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
|
||||
|
||||
return (
|
||||
# The indent and opening fence
|
||||
f"{indent}{opening_fence}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
|
||||
# Replace all code blocks in the markdown
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
def _on_page_markdown_with_config(
|
||||
markdown: str,
|
||||
page: Page,
|
||||
*,
|
||||
add_api_references: bool = True,
|
||||
remove_base64_images: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
if DISABLED:
|
||||
return markdown
|
||||
|
||||
if page.file.src_path.endswith(".ipynb"):
|
||||
logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
# logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
markdown = convert_notebook(page.file.abs_src_path)
|
||||
|
||||
# Append API reference links to code blocks
|
||||
markdown = update_markdown_with_imports(markdown)
|
||||
if add_api_references:
|
||||
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Add file path as an attribute to code blocks that are executable.
|
||||
# This file path is used to associate fixtures with the executable code
|
||||
# which can be used in CI to test the docs without making network requests.
|
||||
markdown = _add_path_to_code_blocks(markdown, page)
|
||||
|
||||
if remove_base64_images:
|
||||
# Remove base64 encoded images from markdown
|
||||
markdown = re.sub(r"!\[.*?\]\(data:image/[^;]+;base64,[^)]+\)", "", markdown)
|
||||
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
return _on_page_markdown_with_config(
|
||||
markdown,
|
||||
page,
|
||||
add_api_references=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# redirects
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<link rel="canonical" href="{url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url={url}">
|
||||
</head>
|
||||
<body>
|
||||
Redirecting...
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def write_html(site_dir, old_path, new_path):
|
||||
"""Write an HTML file in the site_dir with a meta redirect to the new page"""
|
||||
# Determine all relevant paths
|
||||
old_path_abs = os.path.join(site_dir, old_path)
|
||||
old_dir_abs = os.path.dirname(old_path_abs)
|
||||
|
||||
# Create parent directories if they don't exist
|
||||
if not os.path.exists(old_dir_abs):
|
||||
os.makedirs(old_dir_abs)
|
||||
|
||||
# Write the HTML redirect file in place of the old file
|
||||
content = HTML_TEMPLATE.format(url=new_path)
|
||||
with open(old_path_abs, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
for page_old, page_new in REDIRECT_MAP.items():
|
||||
page_old = page_old.replace(".ipynb", ".md")
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
old_html_path = File(page_old, "", "", use_directory_urls).dest_path.replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
new_html_path = (
|
||||
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
import nbformat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
|
||||
NOTEBOOK_DIRS = ("docs/how-tos","docs/tutorials")
|
||||
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")
|
||||
|
||||
@@ -19,36 +19,37 @@ BLOCKLIST_COMMANDS = (
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_CASSETTES = (
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/how-tos/many-tools.ipynb"
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/how-tos/many-tools.ipynb"
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_EXECUTION = [
|
||||
# this uses a user provided project name for langsmith
|
||||
"docs/docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
"docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
# this uses langsmith datasets
|
||||
"docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
"docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
# this uses browser APIs
|
||||
"docs/docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
"docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
# these RAG guides use an ollama model
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
# this loads a massive dataset from gcp
|
||||
"docs/docs/tutorials/usaco/usaco.ipynb",
|
||||
"docs/tutorials/usaco/usaco.ipynb",
|
||||
# TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
|
||||
"docs/docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration-functional.ipynb",
|
||||
# TODO: need to update these notebooks to make sure they are runnable in CI
|
||||
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/docs/tutorials/tot/tot.ipynb",
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
"docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/tutorials/tot/tot.ipynb",
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
]
|
||||
|
||||
|
||||
@@ -216,7 +217,7 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {notebook_path}: {e}")
|
||||
|
||||
with open(os.path.join(DOCS_PATH, "notebooks_no_execution.json"), "w") as f:
|
||||
with open("notebooks_no_execution.json", "w") as f:
|
||||
json.dump(NOTEBOOKS_NO_EXECUTION, f)
|
||||
|
||||
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
"""Create the third party page for the documentation."""
|
||||
|
||||
import argparse
|
||||
from typing import List
|
||||
from typing import TypedDict
|
||||
|
||||
import yaml
|
||||
|
||||
MARKDOWN = """\
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
# 🚀 Prebuilt Agents
|
||||
|
||||
LangGraph includes a prebuilt React agent. For more information on how to use it,
|
||||
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
|
||||
|
||||
If you’re looking for other prebuilt libraries, explore the community-built options
|
||||
below. These libraries can extend LangGraph's functionality in various ways.
|
||||
|
||||
## 📚 Available Libraries
|
||||
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
{library_list}
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
Have you built an awesome open-source library using LangGraph? We'd love to feature
|
||||
your project on the official LangGraph documentation pages! 🏆
|
||||
|
||||
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml]({langgraph_url}) file.
|
||||
|
||||
**Guidelines**
|
||||
|
||||
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
|
||||
for JavaScript/TypeScript, etc.) 📦
|
||||
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
|
||||
the Functional API (exposing an `entrypoint`).
|
||||
- The package must include documentation (e.g., a `README.md` or docs site)
|
||||
explaining how to use it.
|
||||
|
||||
We'll review your contribution and merge it in!
|
||||
|
||||
Thanks for contributing! 🚀
|
||||
"""
|
||||
|
||||
|
||||
class ResolvedPackage(TypedDict):
|
||||
name: str
|
||||
"""The name of the package."""
|
||||
repo: str
|
||||
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
|
||||
weekly_downloads: int | None
|
||||
"""The weekly download count of the package."""
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -> str:
|
||||
"""Generate the markdown content for the third party page.
|
||||
|
||||
Args:
|
||||
resolved_packages: A list of resolved package information.
|
||||
language: str
|
||||
|
||||
Returns:
|
||||
The markdown content as a string.
|
||||
"""
|
||||
# Update the URL to the actual file once the initial version is merged
|
||||
if language == "python":
|
||||
langgraph_url = (
|
||||
"https://github.com/langchain-ai/langgraph/blob/main/docs"
|
||||
"/_scripts/third_party_page/packages.yml"
|
||||
)
|
||||
elif language == "js":
|
||||
langgraph_url = (
|
||||
"https://github.com/langchain-ai/langgraphjs/blob/main/docs"
|
||||
"/_scripts/third_party/packages.yml"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid language '{language}'. Expected 'python' or 'js'.")
|
||||
|
||||
sorted_packages = sorted(
|
||||
resolved_packages, key=lambda p: p["weekly_downloads"] or 0, reverse=True
|
||||
)
|
||||
rows = [
|
||||
"| Name | GitHub URL | Description | Weekly Downloads | Stars |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for package in sorted_packages:
|
||||
name = f"**{package['name']}**"
|
||||
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
|
||||
stars_badge = (
|
||||
f"https://img.shields.io/github/stars/{package['repo']}?style=social"
|
||||
)
|
||||
stars = f""
|
||||
downloads = package["weekly_downloads"] or "-"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} | {stars}"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
library_list="\n".join(rows), langgraph_url=langgraph_url
|
||||
)
|
||||
return markdown_content
|
||||
|
||||
|
||||
def main(input_file: str, output_file: str, language: str) -> None:
|
||||
"""Main function to create the third party page.
|
||||
|
||||
Args:
|
||||
input_file: Path to the input YAML file containing resolved package information.
|
||||
output_file: Path to the output file for the third party page.
|
||||
language: The language for which to generate the third party page.
|
||||
"""
|
||||
# Parse the input YAML file
|
||||
with open(input_file, "r") as f:
|
||||
resolved_packages: List[ResolvedPackage] = yaml.safe_load(f)
|
||||
|
||||
markdown_content = generate_markdown(resolved_packages, language)
|
||||
|
||||
# Write the markdown content to the output file
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Create the third party page.")
|
||||
parser.add_argument(
|
||||
"input_file",
|
||||
help="Path to the input YAML file containing resolved package information.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file", help="Path to the output file for the third party page."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
choices=["python", "js"],
|
||||
default="python",
|
||||
help="The language for which to generate the third party page. Defaults to 'python'.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.input_file, args.output_file, args.language)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
"""Retrieve download count for a list of Python packages from PyPI."""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
class Package(TypedDict):
|
||||
"""A TypedDict representing a package"""
|
||||
|
||||
name: str
|
||||
"""The name of the package."""
|
||||
repo: str
|
||||
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
class ResolvedPackage(Package):
|
||||
weekly_downloads: int | None
|
||||
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
PACKAGES_FILE = HERE / "packages.yml"
|
||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
|
||||
|
||||
|
||||
def _get_weekly_downloads(packages: list[Package], fake: bool) -> list[ResolvedPackage]:
|
||||
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
if fake:
|
||||
# To avoid making network requests during testing, return fake download counts
|
||||
for package in packages:
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
"repo": package["repo"],
|
||||
"weekly_downloads": -12345,
|
||||
"description": package["description"],
|
||||
}
|
||||
)
|
||||
return resolved_packages
|
||||
|
||||
for package in packages:
|
||||
# First check if package exists on PyPI
|
||||
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
|
||||
try:
|
||||
pypi_response = requests.get(pypi_url)
|
||||
pypi_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
|
||||
|
||||
# Get first release date
|
||||
pypi_data = pypi_response.json()
|
||||
releases = pypi_data["releases"]
|
||||
first_release_date = None
|
||||
for version_releases in releases.values():
|
||||
if version_releases: # Some versions may be empty lists
|
||||
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
|
||||
if first_release_date is None or upload_time < first_release_date:
|
||||
first_release_date = upload_time
|
||||
|
||||
if first_release_date is None:
|
||||
raise AssertionError(f"Package {package['name']} has no releases yet")
|
||||
|
||||
# If package was published in last 48 hours, skip download stats
|
||||
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
else:
|
||||
num_downloads = None
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
"repo": package["repo"],
|
||||
"weekly_downloads": num_downloads,
|
||||
"description": package["description"],
|
||||
}
|
||||
)
|
||||
|
||||
return resolved_packages
|
||||
|
||||
|
||||
|
||||
def main(output_file: str, fake: bool) -> None:
|
||||
"""Main function to generate package download information.
|
||||
|
||||
Args:
|
||||
output_file: Path to the output YAML file.
|
||||
"""
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
|
||||
|
||||
if not output_file.endswith(".yml"):
|
||||
raise ValueError("Output file must have a .yml extension")
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
f.write("# This file is auto-generated. Do not edit.\n")
|
||||
yaml.dump(resolved_packages, f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate package download information."
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help=(
|
||||
"Path to the output YAML file. Example: python generate_downloads.py "
|
||||
"downloads.yml"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help=(
|
||||
"Generate fake download counts for testing purposes. "
|
||||
"This option will not make any network requests."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.output_file, args.fake)
|
||||
@@ -0,0 +1,38 @@
|
||||
#A list of third-party packages to surface on the third-party page.
|
||||
packages:
|
||||
- name: "trustcall"
|
||||
repo: "hinthornw/trustcall"
|
||||
description: "Tenacious tool calling built on LangGraph."
|
||||
- name: "breeze-agent"
|
||||
repo: "andrestorres123/breeze-agent"
|
||||
description: "A streamlined research system built inspired on STORM and built on LangGraph."
|
||||
- name: "langgraph-supervisor"
|
||||
repo: "langchain-ai/langgraph-supervisor-py"
|
||||
description: "Build supervisor multi-agent systems with LangGraph."
|
||||
- name: "langmem"
|
||||
repo: "langchain-ai/langmem"
|
||||
description: "Build agents that learn and adapt from interactions over time."
|
||||
- name: "langchain-mcp-adapters"
|
||||
repo: "langchain-ai/langchain-mcp-adapters"
|
||||
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
|
||||
- name: "open-deep-research"
|
||||
repo: "langchain-ai/open_deep_research"
|
||||
description: "Open source assistant for iterative web research and report writing."
|
||||
- name: "langgraph-swarm"
|
||||
repo: "langchain-ai/langgraph-swarm-py"
|
||||
description: "Build swarm-style multi-agent systems using LangGraph."
|
||||
- name: "delve-taxonomy-generator"
|
||||
repo: "andrestorres123/delve"
|
||||
description: "A taxonomy generator for unstructured data"
|
||||
- name: "nodeology"
|
||||
repo: "xyin-anl/Nodeology"
|
||||
description: "Enable researcher to build scientific workflows easily with simplified interface."
|
||||
- name: "langgraph-bigtool"
|
||||
repo: "langchain-ai/langgraph-bigtool"
|
||||
description: "Build LangGraph agents with large numbers of tools."
|
||||
- name: "ai-data-science-team"
|
||||
repo: "business-science/ai-data-science-team"
|
||||
description: "An AI-powered data science team of agents to help you perform common data science tasks 10X faster."
|
||||
- name: "langgraph-reflection"
|
||||
repo: "langchain-ai/langgraph-reflection"
|
||||
description: "LangGraph agent that runs a reflection step."
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
eNqNVVtMHFUYXnoleKnGSL01nG4qJHVnd2Z2C+z6oLBQ2yIXYZtKUcnZmcPOwMyc6cxZ2uVmhPqk0M5TE/WpLLtmxQKxalP1SUiqVmyaNkqjyIsxhgRjfCs2eGbZ5VJu3aeZ8//f/33//51/tjfZjgxTxlrOsKwRZECB0BfT6k0a6FQUmeRsQkVEwmK8rrYhNBg15KnnJUJ0M+DxQF12Q41IBtZlwS1g1dPOeVRkmjCCzHgYi7E7ObmdThWeaSa4DWmmMwA4lve5gDObRU+aOp0GVhB9ckZNZDhpVMBUikbsI0neD44WqSCMw85uF1jOhaYpm4TS3wc4IoNyHF4EBRUYFZEb1MgCAgQDFSECYji6HxzBp4EANXAUSEjR7TMaF2HspdUk6wg6LUFSZAI1BjSoIpr/pt0OFpFiR4U0I+NlDjEm1jREGAUSOkVnd1JCUKSjPheXsEmssTXDG4GCgHTCIE3AoqxFrE8jHbLuAiJqsWukBLte2h0r1YaQzkBFbkeJRZQ1CnVdkQVoxz2tlHs4o5khMR2tDafs1hhqgUasL8uyOjx1Meq1Bli31+fmR88wdMKyplCzaB9UUkJPx79aGdCh0EbrMJl7ZCUWwZdW5mDTGqqGQm3DqpLQECRrCBpqse+zledGVCOyiqxksG4tXSa4TOd1c5zbP7aqsBnTBGuoBSomGlsa8hIkxbO8l2GLGZa7lJ2SgrQIkaxBvtj3sYFMne4A6kvQkiRq9sapI+j6tWTm1l6srcq6Oe3YG6+g7ljfnECiC/A8OEZvFa1/CPBcgPUHOA68Uh0aDmZoQuuaMRYyoGa2UEMqs+YnBSmqtSExFVzX9innclsG5VdkVSZMZmWpWfarFfexLDtVuGmmgVQ6NZsx7vX7/VvUpZNBxLps90eHx/B8KNMle3J9HlnTo/QKprc/oyphq6K6Dm6Zv6wtiyl8AMwGCrmTU0XroXGUrJE4VJpme2Hr/GWJGUzRg2A2lgjWg983vkWiA5tkrhzcYjbYNHtDPamM84wsWl/T52aWa2wMHe84Vl7devzVGkmr9tbprWYJGmyXoZXi3ByIYBxR0EjwMBOEgoSYhvQKWcmKxpqy6qPB4deZehzG9C6FIL1zGtZQogEZdDWtlKDgqEg/dgZKUHh9WaN12c8W86XFLB+GXlQi8j6m8kT9aHaZlpYlbn8p038479CVNejRRE5OwXu5jvRvu3iurGr85YffXfj+/L+hOyc95huP7ZmdftLiJ8d/u6TWFz09ytz7++bEzomGmfnpzvM7r+Y+usv/Bfr87dLrt7u6uj6Y++mvy1Xeu9/27Hgo0H9lqG684HHXgbmSmcrAa7V5+bsGfunruzH/3NgVZi4POm9ddM8mDs4+49pRWFmve36Ihv5p+6R5oC8y+d35ghsk3P5fS/9e76niP12tgZvj3O73n63qvTf/yPBIfkVMeaIDbtt9ZOz2R7k9Z2cih/NyB4zfm65d8Py6bV9/Y9Mfew7k/Hwrv3dPkRIsH9nXMF9S91Z3ecHCi5N3Pwz9eLVnu8OxsLDdIZY/lWrOcTj+B1mCCwM=
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVGtMHFUUXoQ0FavRGAt/bCcThCrMMjMsWxarlAIlpVIorKW8bG5nLuzAvJjH8rKmYKkxlbRDUxoTCElZdulKYRGMTaiExpKYtFFrDbAxaeIfpRok0USNreDdZReovDrJJDP3nO+c73znnNvqcUJF5SQxYoATNagARkM/qtHqUWCdDlXtjFuAmkNiXYUFxfZeXeH8rzg0TVbTk5OBzJmBqDkUSeYYMyMJyU4qWYCqCqqh6jopsY3++mZcAA0nNKkWiiqejlEkbUnC8LATOilvxhWJh+gL11Wo4MjKSIiJqAWO6h1AS1AxoRETgQAz8FOVAbTEQj5gZXigs5BIIVIJVRJFqBE80BBn/JTHAQGLCjvvckiqZgyvoToEGAbKGgFFRmI5sdq4Vt3EyUkYC6sCMbxMIF5QC8NbC6FMAJ5zQvcSyvABWeY5BgTsyTUo90CIM6E1ynCt2RsojUAVi5rxeWaYR3JhI1JWxEhzisVM+xoIVQOcyCNtUB2IklsO2sdWG2TA1KI4RKhrhnsJPLjaR1KNvnzAFBQ/FhIojMPoA4pgtYysPld0UeMEaHiyCtemCxlX0qWYKcpsG34ssNooMkZfFeBVOLws8jLES5N0CkFaCZIaDKvEQ7Facxi9FGXrV6Aqo4mD77tRSE1XW12oI/DOV57QkFwpOBzu5n1TjCsbdcf4ogSySRhNY3lAxFD8VIym0klbOkVjufn2gaxQGvu6zRi2K0BUq1BDcsLN9zAOXayFrDdr3bb78ZWyFJSf5wROI0ILgpoV+DVcFpIk/fGbeipQQKoFMrpSbDbbFnGRMlAzRgP1IfEImraHqqTK1s/DibKORjC4bCFW7gArxOu1Lf1XuIUx8U+A2YAhXeZPWA8t6doain1pwWyJW/uvUAxhEp4EszFFbD34/+RbShS3iedq4Za8sU29N+TjDXWe4FjjBvo+QVJHGw5AzuI8mi2WHM+rLSnJrSm0C1KvkwOGlzJTWLUkVfNwKOsgkQUYBySKgytkeLJLj2TmH8oaOE4USSclNEt2gGZOlEToLoYKWk3Dy/CSzqLLToFuBC/KLDVGbaSVTrOSaawtjdzL0hYip6TIF16m5WVxBW7K4PXeglZWQUeTEW/vPrfdFHwi0bu4yF7IPxyZuePsYt6dyduDo+VvXI95tb/w23Otu7I766z6B1/H6x1XO79bNI+V9TdE75vPPeCMLRsYnv6riu7O2Nc01nP3J+fDR1pGzPi/12+Mf/mUMOa+meP9rds3EVdnOpLab7Mm+S534lOzvbeJSyO/X7GD+MRPDulvdttf2jnb1j7je8//cl77+Nm/02oqTv/RWflRuaOt4NljST2+S12nKwsu2Dp+MU3sOTj7IErsmp94RtpzdQTb0XMzffbp7yvoNojPRRe2TEq1P1ujWi53/NNzpvkiz8zd3TZXgW93bvumg3n909hjUfHPz3SM+u81dBVVvPPjwmf7U0v3ftgVzbTfEhgbtRCdt/vjmRenupgk8vziQH1sQ/c1qvSFxKl73v3z7+a2F1h2zTxKfeu5Jnx6YTruB8o29OdDKahdpOnXnbfuP4gwmf4DBVoOEg==
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVF1sFFUUbmlTEYPBYCTGGIahpQY6uzOz28q2prpua2Vr6d+G/gji7czt7tCZucPMncqKgJZiNI3iGAzExBe7P82mlFZRVGqEGgTUpn0huCiYJhp5MvEBmz7VO9vdttilZZ7u3HO+c77znXNuT7wb6oaE1NxBScVQBwImP4bVE9fhfhMauDemQBxCYqShvjnQb+pSsiiEsWaUO51AkxxAxSEdaZLgEJDi7OacCjQMEIRGpAOJ4aRxkFbAgb0YdUHVoMspjuXdJRSdcSI3Lx+kdSRDcqJNA+o0sQqIMFGxfRWSNlE7ihWqA3XQh/bYSCRC2bYIMjBFyLiYUsZAqgoxIwNM+NKH4iEIRFLU8UgIGdgaWULzDBAEqGEGqgISJTVonQ6+LmkllAg77RgJwY6X0sFKdEGoMUCWumFsDmUNA02TJQHYduc+knswzZfBYQ0uNSfsshhSrYqtc94MD2dDmKiqUqzD5XbwwwcYAwNJlYkupA5CKaal7OcXGzQgdJE4TLpjVmwOPLTYBxlWtA4I9c13hQS6ELKiQFfK3J8vvtdNFUsKtOK+hqXp0saFdC4Hxzk8I3cFNsKqYEU7gWzAkXmR5yEJnuVdDFvGsNxQRiUZqkEcsvo5rnRAh4ZGpg0ejZGQ2DR6IqQj8Ocr8fSAfFpfm+nmrZwNkSrSHevbFiiWUDxP+YFKkfilFM+Vs55yjqVq6gKDvnSaQNZmjAR0oBqdpCHVmebHhZCpdkEx4cva9iS9UJZO8suSImEmvRykWfavFXGzLJvcsqynDhWimp0x4vJ4PCvEJcpAbJ216yPiMTwfmKuS9bRnzyOpmklGMLVoaVYxmxXhtXVF/wVuGcyW+8BkZ8ix7cnibGhk4iUUo9tT2bat7L9AMY0pvh/MvSlS2eD/k28uUeEynouFm/OmlvW+J59EuvOMJFqj5LyX5ZqQx2sohrC/1CeJ1Ri5db/ZsLO/WwJWgnNwVBChoAzP+F5gfEAIQaY5tUJWvKptp7duh2+wlWlCHYjMUgCQmVORCmPNUCeraSUEGZkieex0GCPwJm+bddbDlvHbSzuhS+Q6nhZ5N1Pd0jScWab5ZYnYL2XqaX+LrKxOri7lrtvYtzon9eWJx7213z+37tjsj92OV8o+lO98tsHbeKNvYPUaGtx88tfo1Zc+/iT69uSs48HHHq3P+3361tHHNxUMT8bq9uw6F579qqL35PWnJl6rv6nPJHdN5bqvTE6dGDvxW4HXNzGev9vPvbs5dy3zgHyhB+1+uLWk7yeae8QP/EbjRz+8WTBR+OKqf5Vn3ltzuO7OTGtrtTm+8aEvKruujY/iob/7a4oG26sa8/461v6PN1yz7/SXv+D1zWMX17Zob1z/oPf8NzUnT11Y1TRQNJ3/LD1VuTmcX/u15P5jpqji+Yijlhn1+V/d1n+x4rtLN6ZvF/LXjuT9eXX69tiRy22H1wvvX0ak9NnZvJzkE+9Uyrk5Of8BO5PlsA==
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
eNptVQtwE8cZNo80DA0TJtMYplB6Vklhgu98J52eroltGRvbyC/5AabErO/2pEP38t3KtuxRawhhSHFoLpNXPW1JbGMlQjjEkJhiINMnMXEzTdKaMaEtM0mTJnE7KSn0EQrdk+VgD9yMpNvdb///2///9O2eRDvUDVFVFqREBUEdcAgPDHNPQodtUWigvUMyRGGVH6ypDtYPRHVx6sEwQprhy8sDmkipGlSASHGqnNfO5HFhgPLwuybBdJjBVpWPXVygd9tkaBggBA2bj9jebeNUnEtBeGBrwlvWGQQKQ6IDAvyjE6JCGEAhBB0onGhw6kO2XMKmqxK08FED6rb4DjwjqzyUrKmQhkhWJWVRES2kgXQIZLwgAMmAeAKpqpRJjGJaOooQVdIHtfBfvvuIbpsC5DQgBFFLho+F4aHB6aKWgdn8QJIIpBIYNY+5oKsyAQhDg5woiBwhqRyw9lBWDA3oODiusZHOpOm4djoS4cxwFpoezNLERxGVkC0etwqA+yHqkLcOcgttFWIWrbbughzC6PiOeCIMAY9T/Slr+WBYNZA5PL9bLwOOg7huUOFUHucwj4a6RC2X4KEgAQSTuEMKTJfFTEYg1Eggie1waGaXeQxomiTOUMjbZahKKtNR0uJy+3LS6hqJ+68g80Q1JlFUnlcTw7JSCIZyein3sU7SQEBUJCwTUgKYz5CWXh+bu6ABLoKDkBnJmkMzm4fnYlTDPBwAXHVwXkigc2HzMNBlF3t87rweVZAoQzPhr7k9XWbxVjoHxTCU+5V5gY2YwpmH01IbnbcZIj1GciqOYb5AD3GqGhGhOXWlpYUTWlrlAibQ6Gly0sFaSrAHYiEh0imEi9wu0FDBV0pVsNYhhKqDbZuMUFUtybgdbqfH7WUZkqFoiqEYkgspIMY0yVQjim5rh7E2Z6S+Awissz60yQ6qqTrOX+0ppmL+Mm+Fs8xRXGk0b4VSY3ODi3eVtjCdVHFr6aZacVf5ljZ7lHOrsK2W4ovyCcwu2i7yBZQTya6yzaI/1lRSJHtqw6UdikunVdlb1dwea6uvDLR10Zu3tLTwc+m5WJakMwxdNOuhrWd4VhsSVEIobA443PSLOjQ0bBDwkSFcMhQ19gxiHcKJNxIZo+ivrrwl4ezBEqxJ80wT5HMJu52owO5gp+1Owu7wMV4f7SDKAvUpfyZN/R0l+Eo9thNDwDLcNCv5BBeOKhHIJ/13FPsZS+y4kxZ97D4k7NRUA5IZVmZqK1k3Y5FkecnxmX8WqeohoIhd6bTmmbTqO7o6O3guyvPh9g6Z9naxDrEVRjnhRGYL9gErDSZEyoY54KbZ4czKrO6S+Kw0ydAkzZzqJHVcCkmURVzP9HfGpw1z0ImLffJ2AFIjEDt6gk13gz47F6FDGQvWyn0rDOv1ek/fGTQbyoEhXjd7aj7KgHPZMHbZOHk7IBOinzZSnbNoUuTNqbV40OJhOMELWN7B2Fm7k3ZztINxuwVg90Ceddk9P7MckcNRrGZqqo5IA3L4UkIxcypXBp2WxxQ4GKfDhU+aj28SToryMBhtLVGtMxj5hKZDSQX8y/5S0g+4MCSDaf2ZiZJtVUWBcv9rW8m5QiKrtZkLMaGohiIKwlAQ6rgxZpKT1CiPzVKHQzhWXdE284SXdjlYlqXdra28RxC8ZDG2odloX8pu0HLaBJAw93bOPB52FNh8LOuw5RMyKPC4cJvS1+buoRn3//XCu755YElW+lmEPzdv9gbfVN6jl5/+dMP+gu2P/uWNhuT2no0/fnjp2sK+fmpx33fH0X+Wlj/A3HOzmxh/hj93V/KRwunpib+/9eTY/uXLV+sLpcCiYONv3GtWXH3/zdEvxi4c+dvRvhubs3/YMVo9fj30iZsWVwqvFmUfTN07sX4Ds2Qg/znv4IJVf1g7dv7DC1MJNLnwvPbUsqMjOVWB1N7sf0i+yvfF7HUX7r52ccm+NfqVfNveJ3Z8of7396tbcta9O870/667OeeD+H05l3slW+GZFRtrvrHT3jPx155T4o4LdQd7dvbmrnxaX38pPvZ2jD5QsS31zkf/fFY7+FCqRj7d9e+H81yHDp3t/ST6zvb3XPWTfVe//fhnG3966P5vLWYbyNFf1JwmlkVQCBX3nEo2vuh/1zf9DLFs4PHL91154FIwcNaX/NFnE7ng3nXTcfTx9RDf+8uRa/VrVuf/6l//Wzr4Usqx4fsfrn+q+KuvU6vr/lzlcnl8jZFPC+Mj/mNvrV/1g503+hX+o8+rvrYv4uoZP7do5Zbn9f2h8Q/2NP+cqXl7+OTrDb0jziu7vZeoZgOUFrJ91+OOZ0+Mffzo8I2y72Wt9K96YsVkzoppdDnnt+DzJTefnuy+5LpnzR+P79y6ePfw1FfOPTk50v31y6Mbn3+w4oUNaPKxa/FC8YC07zsDR2LniYtlarrXi7KOnLv6E3JxVtb/ARvF0Mk=
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
eNqNVgtQVNcZJpq0aFtD05R2SGrubB52gLvcu+8LYoHlISDPXZ5itod7z929cF/cc3dhcbCJIXGaRJLbqRlH01oFIaFIohiNphDTBGIyMYY6aavJmIfGxHaMM9LqdEal5y67EarTyc7s49zz/f///f//nf/sxqEI1JCgyLeNCLIONcDqeIGMjUMa7AhDpPcOSlAPKdxAdZXP3x/WhJPpIV1XUXZWFlAFq6JCGQhWVpGyInQWGwJ6Fv6tijDmZqBV4aKnFu1db5EgQiAIkSWbWLvewio4lqzjhaUBm6xAhB6CRCcE+EsjBJlAQCZ4DcisgFjlF5ZMwqIpIjTxYQQ1S08mMd+JHBbFeRCAkIB0gLfwQ11RxAALRDEeWo+qMRAflmOpmhiBM5+YoADl8BV3RNCaZrHMV9ZoDzvdNeVtxfYGE/aNSTaOLgMp5icI9UCcuIkBWjAsYVZmNMv6FouosMC0acHrFosPp1WcSKvF0mPp6Vn3P7lYqkUIECR4RZOATgBErPQKenRVJrHSpwMdrrLOr4aZ3YIsA98ml5512ERSOCjGMlB10qGQkiALpiukaxBIeIMHIoJx3/+neN+mKhxErCaocZjFi9kRukJg1ILG85oiEYBAKmQFXmCJRPFiKatAw86xRFEskqph6Wm6AOeWCWhskaCJUxHkIC6yWTEsZ0GDZnXW3kCbhUigldY2yOoYjXsyFIKAw6FOJ6UMhBSkG6MLxf4SYFmI6wZlVuFwDGNPsFtQMwkO8iJu0jDupwxjZTGG2yFUSSAKETg4Z2W8DFRVFOYoZLUhRR6J9580udy8PWyKnsTHR9aN/VWYRH5pVnUUn0qZoK1Oxup+uYvEghdkEZ8yUgSYz6Aa239t/oYK2HbshIyfeGNwznh0PkZBxu4KwFb5FrgEGhsydgNNcjnG5j/XwrIuSNAY8lbfHC6+eSOc3UrTVvfeBY5RVGaN3TGpHVxgDHUtSrIK9mHspAZZRWkXoHHyUiDA8oFWKZeuqPc0OClfjZW3VUSDfHsXH8p3u0BdGVcuVsIaOx+s8nUUoWBlDUm77W6nx804aJK2UlbaSpNsUAZRukGy1uvhpgiMdjjb/Z2Adzj9wSIbqLLWst4qT4E16i1hypwl9oJy1NwIxfrmOhfnKg7QXdaC1uKiGqGtdE2HLcy6FdhRY+XycwjMLhwRuFyrU5dcJasFb7ShMF/y1ISKO2WXRikSU9kciXb4yys6uqnVawIBbj49l8NBUnGGLsrhoczXaEIbIpSDesjod3mYFzSIVDxf4WODuGR6GG0cwDqE7x0dis/ZXVXlNyScOlCINWmMN0Auk7DZiDI8hWyUzUnY7Nk0k005iZIK/4g3HsZ/Swnu9eOxhXgsw6KE5IfYUFhuh9yw95ZiHzfFjjtp0sfjioRdqoIgGWdljDSStXM3DFlaODZ3skhFCwJZ6I6FNcZjqu/s7urk2DDHhSKdEsV0O+xCKwyz/P64CZ4DZhhMiJQQLo7TORrfSehuGOdKkTRFUvThLlLDpRAFScD1jH3GrzlkDDhxsV+9GaAr7RBfiEOOWDeoifkIDUpYsGbsG24cDMP86daghCs7hjAu++GFKATns6FtEnr1ZkDcxS4KjXQl0KTAGScfwIsADWg3x/O0A7idre5WjsEi8jgYjqJsDMfZuUPmRGSxF7OZqqLpJIIsvtP1qHEyUwJd5ozJtdNOuwtnmoMvYlYMc9AXbi1UzBxQDqFqUFQA95K3mPQCNgRJX0x/xlBhU2V+Ran3QCM5X0hklTr3f2JIVpAs8PygD2q4McYwKyphDg9LDQ5iX7X5TcZ+hnLZHQ4Hb+c8nIfnGbIAj6GEt29kN2BO2iEgYu4R1hgL2XMt2Q6H3ZJDSCDX48Jtiv3reHRwbvpPLlpy31PJSbHXYvyenX3aV/HMdiplfCZjhNmU/s5v/qyxr9/94qYNA6eWb1vKXM77OO2jV45FTvx8dnzRXYSQvmXxvnPH1ttnpk5/t/eRT5+965dr/9EEua1My8SlLeTy+yY+Pt5Zerljst/y0UrrjtMbNpa++cH2i0ev/vrfT5x/d/W/Cv64jE998e3icOonbz31wMTmVLJa6Nu3c8vUJmNZxs5Cz473+9W0TWNnj55ue+OD/LKJ7/Sm7FNmK3Y8eSZjgMtmitL1vueShR2FyY3k9uTqL3tn/NGt3ydOvNvf1tzwym/vP9JOouLd9y7t/GSDvvy9XbXnDl6Uf/+rw6/dafscPtR/9usr2pXrn30x88LDK8btkrdoT6Pyl+SLZ84X0QV508NfPPfVTGD1tdQLd/wu5Q8HRjL8E8LPLrc/u226WP3B+bR6v7Gi/Mc/6ju/5NDxv6qP2B/3qheuSlzf5K6If/m6nKk7r6UNWDd/eE/nZ+l3Fyw5eIys/Vul28V46tr/mddzoISqb5j68uHZvA+PPtSxYumbdfccadJuH7/s7/v8xOTZJ1ven/ZNf33o9ean95GXHs1uoZvRzpI8Ku3aRXlU6Dzzvbrrz1xNop6Yuv/45iLr3//zlnfb5LkHr2fscR+SfnrvqZ+8QV1ZJI0t67tjD7yy6siF599Oz+pNn07LOFWy6PF3zjCXtsqpP3zs05Srt5n9XpzUw35F2W5PSvovy+lBcQ==
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
eNrtVnlUFPcdR63GJg+8a+O5blJFZZadvRfqgbtyCSxyyCGKszO/2R3YOZiZZXc5DILGPDzXJh5NkxhEQB7gASKCmKRJqk+bxkg8yGHtU2uOGkVT4m1/sywGqq8vfc/+0dZ5j535ze/7+3yvz3z5lFbnA16gWGZAHcWIgMdwES6ETaXVPMhzAkFcWUUD0c4SlYmW5JQdTp7qnGEXRU4ICw3FOErBcoDBKAXO0qH5aChux8RQ+Mw5gA+m0soSns8GvFQop4EgYDYgyMNkiwvlOAt9MSJcyF3wyDRBJtqBzAUweONlFCMTSHmITM6zDiDZOAXAy4uXwDc0SwCH9MrGiYiGlYwYuEThXRB5gNFwQWIOAcAXIqA5mJDo5CUQpUIpvWNZhz8G0cP5wEkn48tZwnr4HCYrlDMY7TOwATHbH5pkQwAB5ynObyZPFQAMnoIZsDJo2ScJkuVpTDJTSMc4jId4sMKCD5zjYeV4kQI9S5wSPb4HwDilHBbLGQ8uHYOFkPLuDRYmSTE2eXGxVB3YIIoHhM/cB9DXkrXmAFyElsVLiqvtACOg53MBwyvtrCB6G/q3bjeG4wAWFDA4S0B8b72tgOJCZAQgHZgIamG7GOArjLc2FwAOwRxUPqjqOeXdg3Gcg8J9qYbmCCxT528vIsXy6Hat1E4EkoERvQcjBA+DW2AkETGhiR5INEaGKjQGhXKPGxFEjGIckDiIA4NBVXG+/ba+GxyG50IkxE9ib1XP4Ya+Nqzg3RmP4ZbkfpAYj9u9OzGe1mka+77nnYxI0cBbbUp81J1/80d3agWKKox7+wFLGXnrfbcw3y/FHugHAkTeg+AsxPK+razCWTaXAt7O69nZOJltpWflFESRImo2o3kmY3xaKr3AYcu0KBLjFqaSCsYakZroVhii9NqIGMNCBNWr9GqtAdWqEVShVKAKFElXavQ6nLXFJZvd7pSFeZp5pkQqN7/A6kqwuBx6PpbCWJeiIFbLi04nSeq4NHeCmgdUhDnKlJHNABOKWYEBJXW2eGcBkREJ5jGMzRUug9E58yliFpOftEhPGvhkLjUjzi0wCckuYx7rSTXYEjT2yHQxErXZouKiDersvuEZVQZE6Y9Qp9QYlNLV0EsUB2Bsot27Q21Q1/BA4ODoAGVVsGSiUyithKQEfzxa7R8hFZYFP/J5TKUZEtTbnmJ3hshUOlky4GQqpUojQ9VhGlWYWiuLik+pM/ndpDyWj3tTeIwRSMjJ+b38r8btTiYXELWmxzK/XWI+7KQUPpxRCHBzrAAQf1TeunQkqWd4IjHmxp7PDGF5G8ZQBT633l0SoeGwpJgm/zacBxIkdI7QAiyEBm3w7/RyrRbmpURQJaJED0pjAIfflxQ4x/IiIgAcjmbR4+0MoTG39HHNUsOq62CRw+Eowh1OAiQ7rWaWhj6FcBnHAweLEa1uBA5I4KBoCjbB9+sf+/CbQaUWtTxqIbK5gBG8NWplz3W4rwkPJA9SGg+BKo3wOvR4o14slWRj1Ota+5sJoE9AO3S00PLovh+iQinUuXuNEYrwdr4IF9katUanAoRSpUX1aozQqjGj0mpQW3EdasAwFN1tikRMGG4HSLKPbd5qc0ZCRHyMqTkd6UsbxOKb+HCfYQWGIsmqZMDD1nhrcQfrJOCc5EEVxEqKyPA2GXAj9GWEzNdAt5gRmZ+WtKcX7SHJKqUh6/sPuaKqZ65/OKBt8pqhAb5rEPx78EBMit/QoRx96HJa+tUvtv91QQaPjxg6YGCQ7O2dwfwblz9YcqT7s/XNC089WL+pKabZWrynTv+m50JX+OCVJTXbZe+O+yYjl7j53f2NRbdyJgX9fN3dO6/eu7uk7eNTUXfSU3+9+qN3T9wZpHoJ8rlVO7L9RE7ptvnf5CWuu26w7GsLXhC1/dSYYysWjZ9cFbb2QN44plEZq/gg8rmXg569Pk22cmNHy7BfpK6KCj6aL9Q3fTWhfkT5rZFTlu2fMGPuG6P+lHwJayyJm7khh6sPm55WciZz89rYg5o3T1POpXxF0uY5ty+HX/lb19fh3e+cm63tmsZMml0U3nA7/mT784FdCXuNb42ZfWbN6+TKkoorR+s/2fJxdHFZ98sJQxMXu5AZc9JXdwRtvXYvqHyi4XCZ+lDL8qD7rvttsb+XX53w2pZXFH+/8buDn+7fW3CVkf9M8SoXT3lbu6Z/ay7LPbB3mBg6O3DtqfqunWc21AzpDHnLjD2/a/Dak899tb+a33X5k9zKffMsH97cfSM4XrGqduyKKw6+aGbLOY9jgSJl3el0NCiQSz659QY9sqt4yHvGm89IHRoUUL0M/GEWbNeTlEwDdz4RyRQi63uQcTocfUwwOG7giIVbfmGUjWOOf6WOKElqyCWj7ESPy8AKvDvPAjypxjy7KprkMucnuH+qiMJ4m5OGUUne5IVZPvmSBZ+zoODJkhfLJdXSP3h5jJQz5nBhHkEmOBnG82jSUhL9ksn+KSE/FZZPheVTYfm/LSx1ev2TFZa6/1JhqdIq/w+Fpc7wxIWlWkMqgR4Qep1BaVRZSS2JEyQJgIrACUKj1/4nhaVebdVp/j1h6fxnYZkYkXBh7vBDd8bUG48Er6dH8mWvDX/mxbnHywfNO62kPv/NRMuwDhoZcOnqzPAp5LDCr02pabtqZ48KAPqcwGNNmXFjswo8+z9id08ur3Yd+/x21+r7Z79rdul/+MuVLSbt+NHft85/IbtWbVrCBW++u0I3Yv/5879Fxh4/lAWAceRE+kTgvqnRmYmtW0PLxaVVGctHNxOB+zLfFwYGXDOem1Lx5/Ntme8Xda8Sj9aNT/m2Y1RJ+cUhZoo8XtFe02ked710edy9s/K7EQgSuWfS8pxhZxYW4p3D04d0HHll6RdBLT8sd3V/OdX6ZeOkzJvnDr5weM4l/tOV0buLLlrcdPul5vKabXc3rlhVOq6zeOA1VffVB7sq51Uqon/1/fh7y1pmBOXnjCuSN17QxUY3vX5tzYPy2sDy5vXv0DVBt+rYqWW3pl88m9S1fnBbbfPh935ZMSsuc/WzN8C20EUZeQ2biq+P7ZGEE3nFqsUDAwL+AV1TiAE=
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
-1
@@ -1 +0,0 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
eNqdVnlwE9cZBwxJaNqktBCnlGMRpGmJV97VaclxAcuAD4RsZA47gFi9fZIW78UelmSggOvCJDDAchXS0GBbWOAYc9iFcBgyDUkg9dQmDGlNgZS0Q5OhoU2YztBAQ9+upGAP/JFWM5L2vff7zt/3fW/rk7VQkhmBH9zG8AqUKKCghazVJyW4TIWy0tDCQSUi0Ilyn7+yWZWYvskRRRFld24uJTJmQYQ8xZiBwOXWkrkgQim56FlkoaEmERTo+OUhjctNHJRlKgxlkxt7abkJCMgWr6CFyUPxWFxQMQlSIIIJqoKFBAmLqBzap2SZkRWKB9CNRZFqTI4IKktjJVgIQhrj4higlCmYH4qURCmQjeegjZQ6EIGgBlMiEItCCv1JGMNjfnQ4Q0L6GBkIU0w5mEkSWKh7ocpQMq1chHY4gYasvhUWFdwm4BzDMzpSVpCHHDoIUawM0YYiCGw6HCUuGlpCKm+kT8d//ezGlpt4ijMAYagE0v7oGBrKQGLENAylgmUxRcAQaoDnIUngMAqTRQiYEAMwVkBhIxmzrkMPHTGESDQsiRJiRFIYmFpmoMYi4yYKheHDppUr9QQglhkJ0nogD9B6IjJoIbgUAgWhEfx/CNVgMPCAwUfEOydVYf1ozoRqCH+D8JC8FP9msaWgjw5s0cpkBFI0MrIpERFkRWsfWNoHKQAgKgfIA4FG6rUD4TpGzMFoGGJR3bWicuahkQKttQZCEadYpha2pKS0Q5Qoskwqs7lLZYFvS5c/rnvy8HGrXow4ahZe0Tp9yIlpJbnlcdSDPEaa7S6z81AMR+lieBb1FM5SyJ8W0Tg/2f9ApEANUoKn+1trSQm398cIsrbXSwGff4BKSgIRbS8lcQ5bR/99SeUVhoNa0lP+sLn04QNzVjNJmp2HByiW4zzQ9hoddGyAMFSkOA4EpENrJNoz+WEhH1YiWrPDad0nQVlEEwX+vAWJKapcn0BcwO5zyfRkafKVZUi8Nig7UYR40brmQzoHs1iwUtT4FsJixyxWN+ly2xzYTG9lmydtpvKRNByuRJNCDiEqpmdoT4KIytdAutXzSMK7dMJRNLr7aLDgMCYKMsTTXmltC/B0xeMlRR2p6sIFKUzxTJ1hVusymI/WxaI0UGk6UhvlCFedzcoEoQpCnWkR1AO6GeQQzslagiSslvb0USb5rShYAicJnCBPxHBjNjIcgxJq/KYnO5K1EwTx5sMARaiB6A5I2gjjc7o/QoIcYk03/kCNzeVynXo0KKPKiiAuu+XEQJQM+3tDWjj5zYcBaRVNhNwWy6Bxhtb6JqFFwOIk7cBOBEOAdACKCkIr4QqSdpeDIp3BICCP6xMBIC06m6IgKbgMAbrGlLjWl8NRMb3RCqyk3epAkeajWwKwKg39arBI0GOQ8zFRgqxA0Qc9M3APuqMg7jcKUEsWVc2e5i3xtPqRkx5BqGHglsuDswIBEAoEuQIn4Zyulk33Or1WT4W1qqxo6TwfPa/cSXqoeOmMCiVqMVewap2nkqrCSafVac9zuvIcOGkmzKSZxBkmwJLVc4nSksLYMlYKEFygECyz5s2vjYV50lVZsczGiVXT4Nxaycl7ZIoGswMl4XK5GISANEcRKpc5yOkMWShU+uNFdEnI6lIFbxhFg+6Ugtx8DBUnmo1yQbpFcNQieKpBbJkGycdoIwcF5oHjMB8rRm8APp6N56O7FyUTon80pf2MAgtmCzzs24ZyoNYydAHpLSoWi/NkNba0yh+qqVxaMctqLwpHC2lbMa/6ikpnKvbyKBNS6qL9kuC0OXEinQcHYcszqvCB6/+nV0cX4P07HveJqVedJC/IPBMKtfihhBpIawWsoNJoskuwBXE+Z1qV1ukiHFab3UbTQeQiTTrxQjQzM9q+ng8J/VpIUiyqsVqgdUSsBSa3zWY15WMcVZDnQO1kvBCtaUndUu8M8Y1f/8Qg45OFvvfvd1Z6hYtTv9t1d37bjdf2TPKKW56cvA8f1bQ/cqlw+BHHSbD805+p0UXFTfei9MmXd5fvyV5x6MPbtde6X+zpdpuGDl33k8E7Lr61bftVsOlv69/b2rHujZzxK85dyhm76jj19/bY3a/+NUwpfbrjzsYctmsx2dx2FtvQTt3Y/FhO1d3PK14c/d7Wz/cN2Xb9x2XHutfe6lW29zblf/KdtVcXX/ls0+un5InP1u3ccnDEhIa2N6K7kjfufL8l8Ye+6pF1PmvDp7e+N+HPG3LwqV3ZPy0fu+fi6u5PVp+YtSh7ztHVSzbMenb7keELp9z88PiI9aW7roxb5S7b9HHPxqfeiakfnPl35MCCw3NfWOzt6pxbyDT2zU48c7rD93T9iCGvtr7vzB156fzRSWsfH9MwSK1qLF9nWbWg53rj1GOvvb2t+7ojuebOlZFbtspbTFLTjoYL/ww99cM/TrzcedqxO/v8W7f/cnvXCWL973aN/uhMftfJU884/3Pzq/ubJ7y9+d7U0ZJyf97U0TPKflvd8ELr+f3Nrw7v6f7o5rXdZ34z+ixVHZv8uvt59+YnEufbrE+u6q3a/C18xa/zEqcnH7gaueCoHvX4K12LH7u54ciE4TfGqSX5P7iU6N3wUtbGtb4lxJr33702fra7p/e5MVmj5h78x7vrK3ZMFBb+imwelt0yrOjAB6t6brhunVjR2PHLXucgEMn64qz7r/Wfzb+yUF7AN09Z9KeXFz737XFXm5fsHLqm/d7YMf7qc3UzVwe+XD255Be/7616vnX64P3ejwd/sdM8w/PYhR99OUyvmqxB9a9EDjjQ838B05+eOQ==
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1,6 +1,17 @@
|
||||
ERROR_FOUND=0
|
||||
for file in $(find $1 -name "*.ipynb"); do
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
|
||||
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
|
||||
# Adding regexp to ignore base64 strings
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
echo "Errors found in $file"
|
||||
echo "$OUTPUT"
|
||||
ERROR_FOUND=1
|
||||
fi
|
||||
done
|
||||
|
||||
for file in $(find $1 -name "*.md"); do
|
||||
# Adding regexp to ignore base64 strings
|
||||
OUTPUT=$(cat "$file" | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
echo "Errors found in $file"
|
||||
echo "$OUTPUT"
|
||||
@@ -10,4 +21,4 @@ done
|
||||
|
||||
if [ "$ERROR_FOUND" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 🦜🕸️ Companies using LangGraph
|
||||
|
||||
This list of companies using LangGraph and their success stories is compiled from public sources. If your company uses LangGraph, we'd love for you to share your story and add it to the list. You’re also welcome to contribute updates based on publicly available information from other companies, such as blog posts or press releases.
|
||||
|
||||
|
||||
| Company | Industry | Use case | Reference |
|
||||
| --- | --- | --- | --- |
|
||||
| [AirTop](https://www.airtop.ai/) | Software & Technology (GenAI Native) | Browser automation for AI agents | [Case study, 2024](https://blog.langchain.dev/customers-airtop/) |
|
||||
| [AppFolio](https://www.appfolio.com/) | Real Estate | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-appfolio/) |
|
||||
| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) |
|
||||
| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) |
|
||||
| [Cisco Outshift](https://outshift.cisco.com/) | Software & Technology | DevOps | [Blog post, 2025](https://outshift.cisco.com/blog/build-react-agent-application-for-devops-tasks-using-rest-apis) |
|
||||
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
|
||||
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
|
||||
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
|
||||
| [Klarna](https://www.klarna.com/) | Fintech | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/customers-klarna/) |
|
||||
| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) |
|
||||
| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) |
|
||||
| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) |
|
||||
| [OpenRecovery](https://www.openrecovery.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-openrecovery/) |
|
||||
| [Rakuten](https://www.rakuten.com/) | E-commerce / Fintech | Copilot for domain-specific task | [Blog post, 2025](https://rakuten.today/blog/from-ai-hype-to-real-world-tools-rakuten-teams-up-with-langchain.html) |
|
||||
| [Replit](https://replit.com/) | Software & Technology | Code generation | [Blog post, 2024](https://blog.langchain.dev/customers-replit/); [Breakout agent story, 2024](https://www.langchain.com/breakoutagents/replit); [Fireside chat video, 2024](https://www.youtube.com/watch?v=ViykMqljjxU) |
|
||||
| [Rexera](https://www.rexera.com/) | Real Estate (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-rexera/) |
|
||||
| [Tradestack](https://www.tradestack.uk/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-tradestack/) |
|
||||
| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) |
|
||||
| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) |
|
||||
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
|
||||
@@ -92,3 +92,28 @@ Starting from the `LangGraph Platform` view...
|
||||
1. Check/uncheck checkbox to `Automatically update deployment on push to branch`.
|
||||
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
|
||||
1. Pushes in quick succession to a branch will not trigger subsequent updates. In the future, this functionality may be changed/improved.
|
||||
|
||||
## Add or Remove GitHub Repositories
|
||||
|
||||
After installing and authorizing LangChain's `hosted-langserve` GitHub app, repository access for the app can be modified to add new repositories or remove existing repositories. If a new repository is created, it may need to be added explicitly.
|
||||
|
||||
1. From the GitHub profile, navigate to `Settings` > `Applications` > `hosted-langserve` > click `Configure`.
|
||||
1. Under `Repository access`, select `All repositories` or `Only select repositories`. If `Only select repositories` is selected, new repositories must be explicitly added.
|
||||
1. Click `Save`.
|
||||
1. When creating a new deployment, the list of GitHub repositories in the dropdown menu will be updated to reflect the repository access changes.
|
||||
|
||||
## Whitelisting IP Addresses
|
||||
|
||||
All traffic from `LangGraph Platform` deployments created after January 6th 2025 will come through a NAT gateway.
|
||||
This NAT gateway will have several static ip addresses depending on the region you are deploying in. Refer to the table below for the list of IP addresses to whitelist:
|
||||
|
||||
| US | EU |
|
||||
|----------------|----------------|
|
||||
| 35.197.29.146 | 34.13.192.67 |
|
||||
| 34.145.102.123 | 34.147.105.64 |
|
||||
| 34.169.45.153 | 34.90.22.166 |
|
||||
| 34.82.222.17 | 34.147.36.213 |
|
||||
| 35.227.171.135 | 34.32.137.113 |
|
||||
| 34.169.88.30 | 34.91.238.184 |
|
||||
| 34.19.93.202 | 35.204.101.241 |
|
||||
| 34.19.34.50 | 35.204.48.32 |
|
||||
|
||||
@@ -17,7 +17,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
|
||||
...
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "openai:text-embeddings-3-small",
|
||||
"embed": "openai:text-embedding-3-small",
|
||||
"dims": 1536,
|
||||
"fields": ["$"]
|
||||
}
|
||||
@@ -27,7 +27,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
|
||||
|
||||
This configuration:
|
||||
|
||||
- Uses OpenAI's text-embeddings-3-small model for generating embeddings
|
||||
- Uses OpenAI's text-embedding-3-small model for generating embeddings
|
||||
- Sets the embedding dimension to 1536 (matching the model's output)
|
||||
- Indexes all fields in your stored data (`["$"]` means index everything, or specify specific fields like `["text", "metadata.title"]`)
|
||||
|
||||
|
||||
@@ -36,21 +36,20 @@ 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.56,<0.3.0
|
||||
langgraph-checkpoint>=2.0.5,<3.0
|
||||
langgraph>=0.2.56,<0.4.0
|
||||
langgraph-sdk>=0.1.53
|
||||
langgraph-checkpoint>=2.0.15,<3.0
|
||||
langchain-core>=0.2.38,<0.4.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.9.7
|
||||
httpx>=0.25.0
|
||||
tenacity>=8.0.0
|
||||
uvicorn>=0.26.0
|
||||
sse-starlette>=2.1.0
|
||||
sse-starlette>=2.1.0,<2.2.0
|
||||
uvloop>=0.18.0
|
||||
httptools>=0.5.0
|
||||
jsonschema-rs>=0.16.3
|
||||
croniter>=1.0.1
|
||||
jsonschema-rs>=0.20.0
|
||||
structlog>=23.1.0
|
||||
redis>=5.0.0,<6.0.0
|
||||
```
|
||||
|
||||
Example `requirements.txt` file:
|
||||
|
||||
@@ -36,21 +36,20 @@ 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.56,<0.3.0
|
||||
langgraph-checkpoint>=2.0.5,<3.0
|
||||
langgraph>=0.2.56,<0.4.0
|
||||
langgraph-sdk>=0.1.53
|
||||
langgraph-checkpoint>=2.0.15,<3.0
|
||||
langchain-core>=0.2.38,<0.4.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.9.7
|
||||
httpx>=0.25.0
|
||||
tenacity>=8.0.0
|
||||
uvicorn>=0.26.0
|
||||
sse-starlette>=2.1.0
|
||||
sse-starlette>=2.1.0,<2.2.0
|
||||
uvloop>=0.18.0
|
||||
httptools>=0.5.0
|
||||
jsonschema-rs>=0.16.3
|
||||
croniter>=1.0.1
|
||||
jsonschema-rs>=0.20.0
|
||||
structlog>=23.1.0
|
||||
redis>=5.0.0,<6.0.0
|
||||
```
|
||||
|
||||
Example `pyproject.toml` file:
|
||||
@@ -65,7 +64,7 @@ license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<3.13"
|
||||
python = ">=3.9"
|
||||
langgraph = "^0.2.0"
|
||||
langchain-fireworks = "^0.1.3"
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
# How to implement Generative User Interfaces with LangGraph
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
- [`useStream()` React Hook](./use_stream_react.md)
|
||||
|
||||
Generative user interfaces (Generative UI) allows agents to go beyond text and generate rich user interfaces. This enables creating more interactive and context-aware applications where the UI adapts based on the conversation flow and AI responses.
|
||||
|
||||

|
||||
|
||||
LangGraph Platform supports colocating your React components with your graph code. This allows you to focus on building specific UI components for your graph while easily plugging into existing chat interfaces such as [Agent Chat](https://agentchat.vercel.app) and loading the code only when actually needed.
|
||||
|
||||
!!! warning "LangGraph.js only"
|
||||
|
||||
Currently only LangGraph.js supports Generative UI. Support for Python is coming soon.
|
||||
|
||||
## Tutorial
|
||||
|
||||
### 1. Define and configure UI components
|
||||
|
||||
First, create your first UI component. For each component you need to provide an unique identifier that will be used to reference the component in your graph code.
|
||||
|
||||
```tsx title="src/agent/ui.tsx"
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
return <div>Weather for {props.city}</div>;
|
||||
};
|
||||
|
||||
export default {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
```
|
||||
|
||||
Next, define your UI components in your `langgraph.json` configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/index.ts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ui` section points to the UI components that will be used by graphs. By default, we recommend using the same key as the graph name, but you can split out the components however you like, see [Customise the namespace of UI components](#customise-the-namespace-of-ui-components) for more details.
|
||||
|
||||
LangGraph Platform will automatically bundle your UI components code and styles and serve them as external assets that can be loaded by the `LoadExternalComponent` component. Some dependencies such as `react` and `react-dom` will be automatically excluded from the bundle.
|
||||
|
||||
CSS and Tailwind 4.x is also supported out of the box, so you can freely use Tailwind classes as well as `shadcn/ui` in your UI components.
|
||||
|
||||
=== "`src/agent/ui.tsx`"
|
||||
|
||||
```tsx
|
||||
import "./styles.css";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
return <div className="bg-red-500">Weather for {props.city}</div>;
|
||||
};
|
||||
|
||||
export default {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
```
|
||||
|
||||
=== "`src/agent/styles.css`"
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
```
|
||||
|
||||
### 2. Send the UI components in your graph
|
||||
|
||||
Use the `typedUi` utility to emit UI elements from your agent nodes:
|
||||
|
||||
```typescript title="src/agent/index.ts"
|
||||
import {
|
||||
typedUi,
|
||||
uiMessageReducer,
|
||||
} from "@langchain/langgraph-sdk/react-ui/server";
|
||||
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import type ComponentMap from "./ui.js";
|
||||
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
StateGraph,
|
||||
type LangGraphRunnableConfig,
|
||||
} from "@langchain/langgraph";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
ui: Annotation({ reducer: uiMessageReducer, default: () => [] }),
|
||||
});
|
||||
|
||||
export const graph = new StateGraph(AgentState)
|
||||
.addNode("weather", async (state, config) => {
|
||||
// Provide the type of the component map to ensure
|
||||
// type safety of `ui.push()` calls as well as
|
||||
// pushing the messages to the `ui` and sending a custom event as well.
|
||||
const ui = typedUi<typeof ComponentMap>(config);
|
||||
|
||||
const weather = await new ChatOpenAI({ model: "gpt-4o-mini" })
|
||||
.withStructuredOutput(z.object({ city: z.string() }))
|
||||
.withConfig({ tags: ["langsmith:nostream"] })
|
||||
.invoke(state.messages);
|
||||
|
||||
const response = {
|
||||
id: uuidv4(),
|
||||
type: "ai",
|
||||
content: `Here's the weather for ${weather.city}`,
|
||||
};
|
||||
|
||||
// Emit UI elements with associated AI message
|
||||
ui.push({ name: "weather", props: weather }, { message: response });
|
||||
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge("__start__", "weather")
|
||||
.compile();
|
||||
```
|
||||
|
||||
### 3. Handle UI elements in your React application
|
||||
|
||||
On the client side, you can use `useStream()` and `LoadExternalComponent` to display the UI elements.
|
||||
|
||||
```tsx title="src/app/page.tsx"
|
||||
"use client";
|
||||
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
export default function Page() {
|
||||
const { thread, values } = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>
|
||||
{message.content}
|
||||
{values.ui
|
||||
?.filter((ui) => ui.metadata?.message_id === message.id)
|
||||
.map((ui) => (
|
||||
<LoadExternalComponent key={ui.id} stream={thread} message={ui} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Behind the scenes, `LoadExternalComponent` will fetch the JS and CSS for the UI components from LangGraph Platform and render them in a shadow DOM, thus ensuring style isolation from the rest of your application.
|
||||
|
||||
## How-to guides
|
||||
|
||||
### Show loading UI when components are loading
|
||||
|
||||
You can provide a fallback UI to be rendered when the components are loading.
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
fallback={<div>Loading...</div>}
|
||||
/>
|
||||
```
|
||||
|
||||
### Provide custom components on the client side
|
||||
|
||||
If you already have the components loaded in your client application, you can provide a map of such components to be rendered directly without fetching the UI code from LangGraph Platform.
|
||||
|
||||
```tsx
|
||||
const clientComponents = {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
components={clientComponents}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Customise the namespace of UI components.
|
||||
|
||||
By default `LoadExternalComponent` will use the `assistantId` from `useStream()` hook to fetch the code for UI components. You can customise this by providing a `namespace` prop to the `LoadExternalComponent` component.
|
||||
|
||||
=== "`src/app/page.tsx`"
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
namespace="custom-namespace"
|
||||
/>
|
||||
```
|
||||
|
||||
=== "`langgraph.json`"
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": {
|
||||
"custom-namespace": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access and interact with the thread state from the UI component
|
||||
|
||||
You can access the thread state inside the UI component by using the `useStreamContext` hook.
|
||||
|
||||
```tsx
|
||||
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
const { thread, submit } = useStreamContext();
|
||||
return (
|
||||
<>
|
||||
<div>Weather for {props.city}</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const newMessage = {
|
||||
type: "human",
|
||||
content: `What's the weather in ${props.city}?`,
|
||||
};
|
||||
|
||||
submit({ messages: [newMessage] });
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Pass additional context to the client components
|
||||
|
||||
You can pass additional context to the client components by providing a `meta` prop to the `LoadExternalComponent` component.
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />
|
||||
```
|
||||
|
||||
Then, you can access the `meta` prop in the UI component by using the `useStreamContext` hook.
|
||||
|
||||
```tsx
|
||||
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
const { meta } = useStreamContext<
|
||||
{ city: string },
|
||||
{ MetaType: { userId?: string } }
|
||||
>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
Weather for {props.city} (user: {meta?.userId})
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Streaming UI updates before the node execution is finished
|
||||
|
||||
You can stream UI updates before the node execution is finished by using the `onCustomEvent` callback of the `useStream()` hook.
|
||||
|
||||
```tsx
|
||||
import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const { thread, submit } = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
onCustomEvent: (event, options) => {
|
||||
options.mutate((prev) => {
|
||||
const ui = uiMessageReducer(prev.ui ?? [], event);
|
||||
return { ...prev, ui };
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Remove UI messages from state
|
||||
|
||||
Similar to how messages can be removed from the state by appending a RemoveMessage you can remove an UI message from the state by calling `ui.delete` with the ID of the UI message.
|
||||
|
||||
```tsx
|
||||
// pushed message
|
||||
const message = ui.push({ name: "weather", props: { city: "London" } });
|
||||
|
||||
// remove said message
|
||||
ui.delete(message.id);
|
||||
|
||||
// return new state to persist changes
|
||||
return { ui: ui.items };
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
|
||||
@@ -63,7 +63,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Use the search tool to ask the user where they are, then look up the weather there",
|
||||
"content": "Ask the user where they are, then look up the weather there",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -85,8 +85,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
messages: [
|
||||
{
|
||||
role: "human",
|
||||
content: "Use the search tool to ask the user where they are, then look up the weather there"
|
||||
}
|
||||
content: "Ask the user where they are, then look up the weather there" }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -115,7 +114,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\"}]},
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Ask the user where they are, then look up the weather there\"}]},
|
||||
\"interrupt_before\": [\"ask_human\"],
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 578 KiB |
@@ -0,0 +1,140 @@
|
||||
# Prompt Engineering in LangGraph Studio
|
||||
|
||||
## Overview
|
||||
|
||||
A central aspect of agent development is prompt engineering. LangGraph Studio makes it easy to iterate on the prompts used within your graph directly within the UI.
|
||||
|
||||
## Setup
|
||||
|
||||
The first step is to define your [configuration](https://langchain-ai.github.io/langgraph/how-tos/configuration/) such that LangGraph Studio is aware of the prompts you want to iterate on and which nodes they are associated with.
|
||||
|
||||
### Reference
|
||||
|
||||
When defining your configuration, you can use special metadata keys to instruct LangGraph Studio how to handle different fields. Here's a reference for the available configuration options:
|
||||
|
||||
#### `langgraph_nodes`
|
||||
|
||||
- **Description**: Specifies which graph nodes a configuration field is associated with.
|
||||
- **Value Type**: Array of strings, where each string is the name of a node in your graph.
|
||||
- **Usage Context**: Include in the `json_schema_extra` dictionary for Pydantic models or the `metadata["json_schema_extra"]` dictionary for dataclasses.
|
||||
- **Required**: No, but necessary if you want a field to be editable for specific nodes in the UI.
|
||||
- **Example**:
|
||||
```python
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
json_schema_extra={"langgraph_nodes": ["call_model", "other_node"]},
|
||||
)
|
||||
```
|
||||
|
||||
#### `langgraph_type`
|
||||
|
||||
- **Description**: Specifies the type of configuration field, which determines how it's handled in the UI.
|
||||
- **Value Type**: String
|
||||
- **Supported Values**:
|
||||
- `"prompt"`: Indicates the field contains prompt text that should be treated specially in the UI.
|
||||
- **Usage Context**: Include in the `json_schema_extra` dictionary for Pydantic models or the `metadata["json_schema_extra"]` dictionary for dataclasses.
|
||||
- **Required**: No, but helpful for prompt fields to enable special handling.
|
||||
- **Example**:
|
||||
```python
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
json_schema_extra={
|
||||
"langgraph_nodes": ["call_model"],
|
||||
"langgraph_type": "prompt",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
For example, if you have a node called `call_model` whose system prompt you want to iterate on, you can define a configuration like the following.
|
||||
|
||||
```python
|
||||
## Using Pydantic
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Annotated, Literal
|
||||
|
||||
class Configuration(BaseModel):
|
||||
"""The configuration for the agent."""
|
||||
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
description="The system prompt to use for the agent's interactions. "
|
||||
"This prompt sets the context and behavior for the agent.",
|
||||
json_schema_extra={
|
||||
"langgraph_nodes": ["call_model"],
|
||||
"langgraph_type": "prompt",
|
||||
},
|
||||
)
|
||||
|
||||
model: Annotated[
|
||||
Literal[
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
"anthropic/claude-3-5-haiku-latest",
|
||||
"openai/o1",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/o1-mini",
|
||||
"openai/o3-mini",
|
||||
],
|
||||
{"__template_metadata__": {"kind": "llm"}},
|
||||
] = Field(
|
||||
default="openai/gpt-4o-mini",
|
||||
description="The name of the language model to use for the agent's main interactions. "
|
||||
"Should be in the form: provider/model-name.",
|
||||
json_schema_extra={"langgraph_nodes": ["call_model"]},
|
||||
)
|
||||
|
||||
## Using Dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Configuration:
|
||||
"""The configuration for the agent."""
|
||||
|
||||
system_prompt: str = field(
|
||||
default="You are a helpful AI assistant.",
|
||||
metadata={
|
||||
"description": "The system prompt to use for the agent's interactions. "
|
||||
"This prompt sets the context and behavior for the agent.",
|
||||
"json_schema_extra": {"langgraph_nodes": ["call_model"]},
|
||||
},
|
||||
)
|
||||
|
||||
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
||||
default="anthropic/claude-3-5-sonnet-20240620",
|
||||
metadata={
|
||||
"description": "The name of the language model to use for the agent's main interactions. "
|
||||
"Should be in the form: provider/model-name.",
|
||||
"json_schema_extra": {"langgraph_nodes": ["call_model"]},
|
||||
},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
## Iterating on prompts
|
||||
|
||||
### Node Configuration
|
||||
|
||||
With this set up, running your graph and viewing in LangGraph Studio will result in the graph rendering like such.
|
||||
|
||||
**Note the configuration icon in the top right corner of the `call_model` node**:
|
||||
|
||||
{width=1200}
|
||||
|
||||
Clicking this icon will open a modal where you can edit the configuration for all of the fields associated with the `call_model` node. From here, you can save your changes and apply them to the graph. Note that these values reflect the currently active assistant, and saving will update the assistant with the new values.
|
||||
|
||||
{width=1200}
|
||||
|
||||
### Playground
|
||||
|
||||
LangGraph Studio also supports prompt engineering through an integration with the LangSmith Playground. To do so:
|
||||
|
||||
1. Open an existing thread or create a new one.
|
||||
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
|
||||
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
|
||||
|
||||
{width=1200}
|
||||
|
||||
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
|
||||
|
||||
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -99,7 +99,7 @@ We can stream the results of a stateless run in an almost identical fashion to h
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--url <DEPLOYMENT_URL>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
@@ -144,7 +144,7 @@ In addition to streaming, you can also wait for a stateless result by using the
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/runs/runs/wait \
|
||||
--url <DEPLOYMENT_URL>/runs/wait \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_IDD>,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Test Cloud Deployment
|
||||
# Test LangGraph Platform Deployment
|
||||
|
||||
The LangGraph Studio UI connects directly to LangGraph Cloud deployments.
|
||||
The LangGraph Studio UI connects directly to LangGraph Platform deployments.
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Platform`. The `LangGraph Platform` view contains a list of existing LangGraph Platform deployments.
|
||||
1. Select an existing deployment to test with LangGraph Studio.
|
||||
1. In the top-right corner, select `Open LangGraph Studio`.
|
||||
1. [Invoke an assistant](./invoke_studio.md) or [view an existing thread](./threads_studio.md).
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
# How to integrate LangGraph into your React application
|
||||
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
|
||||
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
|
||||
|
||||
Key features:
|
||||
|
||||
- Messages streaming: Handle a stream of message chunks to form a complete message
|
||||
- Automatic state management for messages, interrupts, loading states, and errors
|
||||
- Conversation branching: Create alternate conversation paths from any point in the chat history
|
||||
- UI-agnostic design: bring your own components and styling
|
||||
|
||||
Let's explore how to use `useStream()` in your React application.
|
||||
|
||||
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we also recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @langchain/langgraph-sdk @langchain/core
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [{ type: "human", content: message }] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button keytype="submit">Send</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing Your UI
|
||||
|
||||
The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
|
||||
|
||||
- Thread state management
|
||||
- Loading and error states
|
||||
- Interrupts
|
||||
- Message handling and updates
|
||||
- Branching support
|
||||
|
||||
Here are some examples on how to use these features effectively:
|
||||
|
||||
### Loading States
|
||||
|
||||
The `isLoading` property tells you when a stream is active, enabling you to:
|
||||
|
||||
- Show a loading indicator
|
||||
- Disable input fields during processing
|
||||
- Display a cancel button
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
const { isLoading, stop } = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<form>
|
||||
{isLoading && (
|
||||
<button key="stop" type="button" onClick={() => stop()}>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Management
|
||||
|
||||
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
|
||||
|
||||
```tsx
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
|
||||
threadId: threadId,
|
||||
onThreadId: setThreadId,
|
||||
});
|
||||
```
|
||||
|
||||
We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
|
||||
|
||||
### Messages Handling
|
||||
|
||||
The `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
|
||||
|
||||
By default, the `messagesKey` is set to `messages`, where it will append the new messages chunks to `values["messages"]`. If you store messages in a different key, you can change the value of `messagesKey`.
|
||||
|
||||
```tsx
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
export default function HomePage() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Under the hood, the `useStream()` hook will use the `streamMode: "messages-key"` to receive a stream of messages (i.e. individual LLM tokens) from any LangChain chat model invocations inside your graph nodes. Learn more about messages streaming in the [How to stream messages from your graph](./stream_messages.md) guide.
|
||||
|
||||
### Interrupts
|
||||
|
||||
The `useStream()` hook exposes the `interrupt` property, which will be filled with the last interrupt from the thread. You can use interrupts to:
|
||||
|
||||
- Render a confirmation UI before executing a node
|
||||
- Wait for human input, allowing agent to ask the user with clarifying questions
|
||||
|
||||
Learn more about interrupts in the [How to handle interrupts](../../how-tos/human_in_the_loop/wait-user-input.ipynb) guide.
|
||||
|
||||
```tsx
|
||||
const thread = useStream<
|
||||
{ messages: Message[] },
|
||||
{ InterruptType: string }
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
if (thread.interrupt) {
|
||||
return (
|
||||
<div>
|
||||
Interrupted! {thread.interrupt.value}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// `resume` can be any value that the agent accepts
|
||||
thread.submit(undefined, { command: { resume: true } });
|
||||
}}
|
||||
>
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Branching
|
||||
|
||||
For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
|
||||
|
||||
A branch can be created in following ways:
|
||||
|
||||
1. Edit a previous user message.
|
||||
2. Request a regeneration of a previous assistant message.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { useState } from "react";
|
||||
|
||||
function BranchSwitcher({
|
||||
branch,
|
||||
branchOptions,
|
||||
onSelect,
|
||||
}: {
|
||||
branch: string | undefined;
|
||||
branchOptions: string[] | undefined;
|
||||
onSelect: (branch: string) => void;
|
||||
}) {
|
||||
if (!branchOptions || !branch) return null;
|
||||
const index = branchOptions.indexOf(branch);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const prevBranch = branchOptions[index - 1];
|
||||
if (!prevBranch) return;
|
||||
onSelect(prevBranch);
|
||||
}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span>
|
||||
{index + 1} / {branchOptions.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const nextBranch = branchOptions[index + 1];
|
||||
if (!nextBranch) return;
|
||||
onSelect(nextBranch);
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessage({
|
||||
message,
|
||||
onEdit,
|
||||
}: {
|
||||
message: Message;
|
||||
onEdit: (message: Message) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<button type="button" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const content = new FormData(form).get("content") as string;
|
||||
|
||||
form.reset();
|
||||
onEdit({ type: "human", content });
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
<input name="content" defaultValue={message.content as string} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => {
|
||||
const meta = thread.getMessagesMetadata(message);
|
||||
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
<div>{message.content as string}</div>
|
||||
|
||||
{message.type === "human" && (
|
||||
<EditMessage
|
||||
message={message}
|
||||
onEdit={(message) =>
|
||||
thread.submit(
|
||||
{ messages: [message] },
|
||||
{ checkpoint: parentCheckpoint },
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.type === "ai" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
thread.submit(undefined, { checkpoint: parentCheckpoint })
|
||||
}
|
||||
>
|
||||
<span>Regenerate</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<BranchSwitcher
|
||||
branch={meta?.branch}
|
||||
branchOptions={meta?.branchOptions}
|
||||
onSelect={(branch) => thread.setBranch(branch)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [message] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button key="submit" type="submit">
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For advanced use cases you can use the `experimental_branchTree` property to get the tree representation of the thread, which can be used to render branching controls for non-message based graphs.
|
||||
|
||||
### TypeScript
|
||||
|
||||
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
|
||||
|
||||
```tsx
|
||||
// Define your types
|
||||
type State = {
|
||||
messages: Message[];
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// Use them with the hook
|
||||
const thread = useStream<State>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
You can also optionally specify types for different scenarios, such as:
|
||||
|
||||
- `ConfigurableType`: Type for the `config.configurable` property (default: `Record<string, unknown>`)
|
||||
- `InterruptType`: Type for the interrupt value - i.e. contents of `interrupt(...)` function (default: `unknown`)
|
||||
- `CustomEventType`: Type for the custom events (default: `unknown`)
|
||||
- `UpdateType`: Type for the submit function (default: `Partial<State>`)
|
||||
|
||||
```tsx
|
||||
|
||||
const thread = useStream<State, {
|
||||
UpdateType: {
|
||||
messages: Message[] | Message;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
InterruptType: string;
|
||||
CustomEventType: {
|
||||
type: "progress" | "debug";
|
||||
payload: unknown;
|
||||
};
|
||||
ConfigurableType: {
|
||||
model: string;
|
||||
};
|
||||
}>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
If you're using LangGraph.js, you can also reuse your graph's annotation types. However, make sure to only import the types of the annotation schema in order to avoid importing the entire LangGraph.js runtime (i.e. via `import type { ... }` directive).
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
type StateType,
|
||||
type UpdateType,
|
||||
} from "@langchain/langgraph/web";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
context: Annotation<string>(),
|
||||
});
|
||||
|
||||
const thread = useStream<
|
||||
StateType<typeof AgentState.spec>,
|
||||
{ UpdateType: UpdateType<typeof AgentState.spec> }
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
The `useStream()` hook provides several callback options to help you respond to different events:
|
||||
|
||||
- `onError`: Called when an error occurs.
|
||||
- `onFinish`: Called when the stream is finished.
|
||||
- `onUpdateEvent`: Called when an update event is received.
|
||||
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
|
||||
- `onMetadataEvent`: Called when a metadata event is received, which contains the Run ID and Thread ID.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
|
||||
+119
-114
@@ -1,142 +1,147 @@
|
||||
# Use Webhooks
|
||||
# Using Webhooks
|
||||
|
||||
You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the "webhook" parameter.
|
||||
When working with LangGraph Cloud, you may want to use webhooks to receive updates after an API call completes. Webhooks are useful for triggering actions in your service once a run has finished processing. To implement this, you need to expose an endpoint that can accept `POST` requests and pass this endpoint as a `webhook` parameter in your API request.
|
||||
|
||||
Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.
|
||||
Currently, the SDK does not provide built-in support for defining webhook endpoints, but you can specify them manually using API requests.
|
||||
|
||||
The following endpoints accept `webhook` as a parameter:
|
||||
## Supported Endpoints
|
||||
|
||||
- Create Run -> POST /thread/{thread_id}/runs
|
||||
- Create Thread Cron -> POST /thread/{thread_id}/runs/crons
|
||||
- Stream Run -> POST /thread/{thread_id}/runs/stream
|
||||
- Wait Run -> POST /thread/{thread_id}/runs/wait
|
||||
- Create Cron -> POST /runs/crons
|
||||
- Stream Run Stateless -> POST /runs/stream
|
||||
- Wait Run Stateless -> POST /runs/wait
|
||||
The following API endpoints accept a `webhook` parameter:
|
||||
|
||||
In this example, we will show calling a webhook after streaming a run.
|
||||
| Operation | HTTP Method | Endpoint |
|
||||
|-----------|------------|----------|
|
||||
| Create Run | `POST` | `/thread/{thread_id}/runs` |
|
||||
| Create Thread Cron | `POST` | `/thread/{thread_id}/runs/crons` |
|
||||
| Stream Run | `POST` | `/thread/{thread_id}/runs/stream` |
|
||||
| Wait Run | `POST` | `/thread/{thread_id}/runs/wait` |
|
||||
| Create Cron | `POST` | `/runs/crons` |
|
||||
| Stream Run Stateless | `POST` | `/runs/stream` |
|
||||
| Wait Run Stateless | `POST` | `/runs/wait` |
|
||||
|
||||
## Setup
|
||||
In this guide, we’ll show how to trigger a webhook after streaming a run.
|
||||
|
||||
First, let's setup our assistant and thread:
|
||||
## Setting Up Your Assistant and Thread
|
||||
|
||||
Before making API calls, set up your assistant and thread.
|
||||
|
||||
=== "Python"
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
=== "JavaScript"
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread);
|
||||
```
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantID = "agent";
|
||||
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 }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
### Example Response
|
||||
```json
|
||||
{
|
||||
"thread_id": "9dde5490-2b67-47c8-aa14-4bfec88af217",
|
||||
"created_at": "2024-08-30T23:07:38.242730+00:00",
|
||||
"updated_at": "2024-08-30T23:07:38.242730+00:00",
|
||||
"metadata": {},
|
||||
"status": "idle",
|
||||
"config": {},
|
||||
"values": null
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
## Using a Webhook with a Graph Run
|
||||
|
||||
{
|
||||
'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217',
|
||||
'created_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'updated_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
To use a webhook, specify the `webhook` parameter in your API request. When the run completes, LangGraph Cloud sends a `POST` request to the specified webhook URL.
|
||||
|
||||
## Use graph with a webhook
|
||||
|
||||
To invoke a run with a webhook, we specify the `webhook` parameter with the desired endpoint when creating a run. Webhook requests are triggered by the end of a run.
|
||||
|
||||
For example, if we can receive requests at `https://my-server.app/my-webhook-endpoint`, we can pass this to `stream`:
|
||||
For example, if your server listens for webhook events at `https://my-server.app/my-webhook-endpoint`, include this in your request:
|
||||
|
||||
=== "Python"
|
||||
```python
|
||||
input = { "messages": [{ "role": "user", "content": "Hello!" }] }
|
||||
|
||||
```python
|
||||
# create input
|
||||
input = { "messages": [{ "role": "user", "content": "Hello!" }] }
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="events",
|
||||
webhook="https://my-server.app/my-webhook-endpoint"
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="events",
|
||||
webhook="https://my-server.app/my-webhook-endpoint"
|
||||
):
|
||||
# Do something with the stream output
|
||||
pass
|
||||
```
|
||||
=== "JavaScript"
|
||||
```js
|
||||
const input = { messages: [{ role: "human", content: "Hello!" }] };
|
||||
|
||||
=== "Javascript"
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantID,
|
||||
{
|
||||
input: input,
|
||||
webhook: "https://my-server.app/my-webhook-endpoint"
|
||||
}
|
||||
);
|
||||
|
||||
```js
|
||||
// create input
|
||||
const input = { messages: [{ role: "human", content: "Hello!" }] };
|
||||
|
||||
// stream events
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantID,
|
||||
{
|
||||
input: input,
|
||||
webhook: "https://my-server.app/my-webhook-endpoint"
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
// Do something with the stream output
|
||||
}
|
||||
```
|
||||
for await (const chunk of streamResponse) {
|
||||
// Handle stream output
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```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": "Hello!"}]},
|
||||
"webhook": "https://my-server.app/my-webhook-endpoint"
|
||||
}'
|
||||
```
|
||||
|
||||
The schema for the payload sent to `my-webhook-endpoint` is that of a [run](../../concepts/langgraph_server.md/#runs). See [API Reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/api_ref.html#model/run) for more detail. Note that the run input, configuration, etc. are included in the `kwargs` field.
|
||||
|
||||
### Signing webhook requests
|
||||
|
||||
To sign the webhook requests, we can specify a token parameter in the webhook URL, e.g.,
|
||||
```
|
||||
https://my-server.app/my-webhook-endpoint?token=...
|
||||
```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": "Hello!"}]},
|
||||
"webhook": "https://my-server.app/my-webhook-endpoint"
|
||||
}'
|
||||
```
|
||||
|
||||
The server should then extract the token from the request's parameters and validate it before processing the payload.
|
||||
## Webhook Payload
|
||||
|
||||
LangGraph Cloud sends webhook notifications in the format of a [Run](../../concepts/langgraph_server.md/#runs). See the [API Reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/api_ref.html#model/run) for details. The request payload includes run input, configuration, and other metadata in the `kwargs` field.
|
||||
|
||||
## Securing Webhooks
|
||||
|
||||
To ensure only authorized requests hit your webhook endpoint, consider adding a security token as a query parameter:
|
||||
|
||||
```
|
||||
https://my-server.app/my-webhook-endpoint?token=YOUR_SECRET_TOKEN
|
||||
```
|
||||
|
||||
Your server should extract and validate this token before processing requests.
|
||||
|
||||
## Testing Webhooks
|
||||
|
||||
You can test your webhook using online services like:
|
||||
|
||||
- **[Beeceptor](https://beeceptor.com/)** – Quickly create a test endpoint and inspect incoming webhook payloads.
|
||||
- **[Webhook.site](https://webhook.site/)** – View, debug, and log incoming webhook requests in real time.
|
||||
|
||||
These tools help you verify that LangGraph Cloud is correctly triggering and sending webhooks to your service.
|
||||
|
||||
---
|
||||
|
||||
By following these steps, you can integrate webhooks into your LangGraph Cloud workflow, automating actions based on completed runs.
|
||||
|
||||
@@ -90,7 +90,7 @@ For this guide, we'll use the pre-built Python [**ReAct Agent**](https://github.
|
||||
</figure>
|
||||
|
||||
|
||||
## Lagraph Studio Web UI
|
||||
## LangGraph Studio Web UI
|
||||
|
||||
Once your application is deployed, you can test it in **LangGraph Studio**.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# LangGraph CLI
|
||||
|
||||
The LangGraph command line interface includes commands to build and run a LangGraph Cloud API server locally in [Docker](https://www.docker.com/). For development and testing, you can use the CLI to deploy a local API server as an alternative to the [Studio desktop app](../../concepts/langgraph_studio.md).
|
||||
The LangGraph command line interface includes commands to build and run a LangGraph Cloud API server locally in [Docker](https://www.docker.com/). For development and testing, you can use the CLI to deploy a local API server.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -51,6 +51,7 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
|
||||
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
|
||||
| <span style="white-space: nowrap;">`pip_config_file`</span> | Path to `pip` config file. |
|
||||
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li></ul> |
|
||||
|
||||
=== "JS"
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
The LangGraph Cloud Server supports specific environment variables for configuring a deployment.
|
||||
|
||||
## `DD_API_KEY`
|
||||
|
||||
Specify `DD_API_KEY` (your [Datadog API Key](https://docs.datadoghq.com/account_management/api-app-keys/)) to automatically enable Datadog tracing for the deployment. Specify other [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) to configure the tracing instrumentation.
|
||||
|
||||
If `DD_API_KEY` is specified, the application process is wrapped in the [`ddtrace-run` command](https://ddtrace.readthedocs.io/en/stable/installation_quickstart.html). Other `DD_*` environment variables (e.g. `DD_SITE`, `DD_ENV`, `DD_SERVICE`, `DD_TRACE_ENABLED`) are typically needed to properly configure the tracing instrumentation. See [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) for more details.
|
||||
|
||||
## `LANGCHAIN_TRACING_SAMPLING_RATE`
|
||||
|
||||
Sampling rate for traces sent to LangSmith. Valid values: Any float between `0` and `1`.
|
||||
|
||||
@@ -14,7 +14,7 @@ As a result, there are many different types of [agent architectures](https://blo
|
||||
|
||||
## Router
|
||||
|
||||
A router allows an LLM to select a single step from a specified set of options. This is an agent architecture that exhibits a relatively limited level of control because the LLM usually focuses on making a single decision and produces a specific output from limited set of pre-defined options. Routers typically employ a few different concepts to achieve this.
|
||||
A router allows an LLM to select a single step from a specified set of options. This is an agent architecture that exhibits a relatively limited level of control because the LLM usually focuses on making a single decision and produces a specific output from a limited set of pre-defined options. Routers typically employ a few different concepts to achieve this.
|
||||
|
||||
### Structured Output
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ Below are examples of directory structures for Python and JavaScript application
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── requirements.txt # package dependencies
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
├── requirements.txt # package dependencies
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
=== "Python (pyproject.toml)"
|
||||
|
||||
@@ -27,12 +27,19 @@ LangGraph Platform provides different security defaults:
|
||||
- Requires valid API key in `x-api-key` header
|
||||
- Can be customized with your auth handler
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth **is supported** for all plans in LangGraph Cloud.
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
- No default authentication
|
||||
- Complete flexibility to implement your security model
|
||||
- You control all aspects of authentication and authorization
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth is supported for **Enterprise** self-hosted plans.
|
||||
Self-hosted lite plans do not support custom auth natively.
|
||||
|
||||
## System Architecture
|
||||
|
||||
A typical authentication setup involves three main components:
|
||||
|
||||
@@ -83,12 +83,12 @@ 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
|
||||
We recommend that you [**use the `interrupt` function instead**][langgraph.types.interrupt] 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.
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of _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:
|
||||
|
||||
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
@@ -49,9 +49,9 @@ For more information, please see:
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
|
||||
With a Self-Hosted Lite deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Durable Execution
|
||||
|
||||
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
|
||||
|
||||
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
|
||||
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
To leverage durable execution in LangGraph, you need to:
|
||||
|
||||
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
|
||||
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||
|
||||
## Determinism and Consistent Replay
|
||||
|
||||
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
|
||||
|
||||
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
|
||||
|
||||
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
|
||||
|
||||
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
|
||||
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
|
||||
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
|
||||
|
||||
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
|
||||
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
|
||||
|
||||
## Using tasks in nodes
|
||||
|
||||
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
|
||||
|
||||
=== "Original"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
url: str
|
||||
result: NotRequired[str]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
result = requests.get(state['url']).text[:100] # Side-effect
|
||||
return {
|
||||
"result": result
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"url": "https://www.example.com"}, config)
|
||||
```
|
||||
|
||||
=== "With task"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import task
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
urls: list[str]
|
||||
result: NotRequired[list[str]]
|
||||
|
||||
|
||||
@task
|
||||
def _make_request(url: str):
|
||||
"""Make a request."""
|
||||
# highlight-next-line
|
||||
return requests.get(url).text[:100]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
requests = [_make_request(url) for url in state['urls']]
|
||||
results = [request.result() for request in requests]
|
||||
return {
|
||||
"results": results
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"urls": ["https://www.example.com"]}, config)
|
||||
```
|
||||
|
||||
## Resuming Workflows
|
||||
|
||||
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
## Starting Points for Resuming Workflows
|
||||
|
||||
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||
@@ -36,7 +36,7 @@ LangGraph is a stateful, orchestration framework that brings added control to ag
|
||||
| Concurrency Control | Simple threading | Supports double-texting |
|
||||
| Scheduling | None | Cron scheduling |
|
||||
| Monitoring | None | Integrated with LangSmith for observability |
|
||||
| IDE integration | LangGraph Studio for Desktop | LangGraph Studio for Desktop & Cloud |
|
||||
| IDE integration | LangGraph Studio | LangGraph Studio |
|
||||
|
||||
## What are my deployment options for LangGraph Platform?
|
||||
|
||||
@@ -62,3 +62,9 @@ Yes! You can use LangGraph with any LLMs. The main reason we use LLMs that suppo
|
||||
## Does LangGraph work with OSS LLMs?
|
||||
|
||||
Yes! LangGraph is totally ambivalent to what LLMs are used under the hood. The main reason we use closed LLMs in most of the tutorials is that they seamlessly support tool calling, while OSS LLMs often don't. But tool calling is not necessary (see [this section](#does-langgraph-work-with-llms-that-dont-support-tool-calling)) so you can totally use LangGraph with OSS LLMs.
|
||||
|
||||
## Can I use LangGraph Studio without logging to LangSmith
|
||||
|
||||
Yes! You can use the [development version of LangGraph Server](../tutorials/langgraph-platform/local-server.md) to run the backend locally.
|
||||
This will connect to the studio frontend hosted as part of LangSmith.
|
||||
If you set an environment variable of `LANGSMITH_TRACING=false` then no traces will be sent to LangSmith.
|
||||
@@ -0,0 +1,936 @@
|
||||
# Functional API
|
||||
|
||||
## Overview
|
||||
|
||||
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
|
||||
|
||||
It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model.
|
||||
|
||||
The Functional API uses two key building blocks:
|
||||
|
||||
- **`@entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts.
|
||||
- **`@task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously.
|
||||
|
||||
This provides a minimal abstraction for building workflows with state management and streaming.
|
||||
|
||||
!!! tip
|
||||
|
||||
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
|
||||
Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms.
|
||||
|
||||
## Example
|
||||
|
||||
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # A placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
```
|
||||
|
||||
??? example "Detailed Explanation"
|
||||
|
||||
This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided.
|
||||
|
||||
When the workflow is resumed, it executes from the very start, but because the result of the `write_essay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed.
|
||||
|
||||
```python
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id
|
||||
}
|
||||
}
|
||||
|
||||
for item in workflow.stream("cat", config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'write_essay': 'An essay about topic: cat'}
|
||||
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
|
||||
```
|
||||
|
||||
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
# Get review from a user (e.g., via a UI)
|
||||
# In this case, we're using a bool, but this can be any json-serializable value.
|
||||
human_review = True
|
||||
|
||||
for item in workflow.stream(Command(resume=human_review), config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}
|
||||
```
|
||||
|
||||
The workflow has been completed and the review has been added to the essay.
|
||||
|
||||
## Entrypoint
|
||||
|
||||
The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./low_level.md#interrupt).
|
||||
|
||||
### Definition
|
||||
|
||||
An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator.
|
||||
|
||||
The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument.
|
||||
|
||||
Decorating a function with an `entrypoint` produces a [`Pregel`][langgraph.pregel.Pregel.stream] instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
|
||||
|
||||
You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**.
|
||||
|
||||
=== "Sync"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop.
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
=== "Async"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details.
|
||||
|
||||
|
||||
### Injectable Parameters
|
||||
|
||||
When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include:
|
||||
|
||||
|
||||
| Parameter | Description |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **previous** | Access the the state associated with the previous `checkpoint` for the given thread. See [state management](#state-management). |
|
||||
| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](#long-term-memory). |
|
||||
| **writer** | For streaming custom data, to write custom data to the `custom` stream. Useful for [streaming custom data](#streaming-custom-data). |
|
||||
| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. |
|
||||
|
||||
!!! important
|
||||
|
||||
Declare the parameters with the appropriate name and type annotation.
|
||||
|
||||
??? example "Requesting Injectable Parameters"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory
|
||||
|
||||
@entrypoint(
|
||||
checkpointer=checkpointer, # Specify the checkpointer
|
||||
store=in_memory_store # Specify the store
|
||||
)
|
||||
def my_workflow(
|
||||
some_input: dict, # The input (e.g., passed via `invoke`)
|
||||
*,
|
||||
previous: Any = None, # For short-term memory
|
||||
store: BaseStore, # For long-term memory
|
||||
writer: StreamWriter, # For streaming custom data
|
||||
config: RunnableConfig # For accessing the configuration passed to the entrypoint
|
||||
) -> ...:
|
||||
```
|
||||
|
||||
### Executing
|
||||
|
||||
Using the [`@entrypoint`](#entrypoint) yields a [`Pregel`][langgraph.pregel.Pregel.stream] object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
my_workflow.invoke(some_input, config) # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
await my_workflow.ainvoke(some_input, config) # Await result asynchronously
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### Resuming
|
||||
|
||||
Resuming an execution after an [interrupt][langgraph.types.interrupt] can be done by passing a **resume** value to the [Command][langgraph.types.Command] primitive.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
**Resuming after an error**
|
||||
|
||||
|
||||
To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config).
|
||||
|
||||
This assumes that the underlying **error** has been resolved and execution can proceed successfully.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(None, config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(None, config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints).
|
||||
|
||||
This allows accessing the state from the previous invocation using the `previous` parameter.
|
||||
|
||||
By default, the `previous` parameter is the return value of the previous invocation.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> int:
|
||||
previous = previous or 0
|
||||
return number + previous
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(1, config) # 1 (previous was None)
|
||||
my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation)
|
||||
```
|
||||
|
||||
#### `entrypoint.final`
|
||||
|
||||
[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**.
|
||||
|
||||
The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
|
||||
previous = previous or 0
|
||||
# This will return the previous value to the caller, saving
|
||||
# 2 * number to the checkpoint, which will be used in the next invocation
|
||||
# for the `previous` parameter.
|
||||
return entrypoint.final(value=previous, save=2 * number)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(3, config) # 0 (previous was None)
|
||||
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
|
||||
```
|
||||
|
||||
## Task
|
||||
|
||||
A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics:
|
||||
|
||||
* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking.
|
||||
* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details).
|
||||
|
||||
### Definition
|
||||
|
||||
Tasks are defined using the `@task` decorator, which wraps a regular Python function.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
@task()
|
||||
def slow_computation(input_value):
|
||||
# Simulate a long-running operation
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **outputs** of tasks must be JSON-serializable to support checkpointing.
|
||||
|
||||
### Execution
|
||||
|
||||
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes).
|
||||
|
||||
Tasks *cannot* be called directly from the main application code.
|
||||
|
||||
When you call a **task**, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later.
|
||||
|
||||
To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`).
|
||||
|
||||
|
||||
=== "Synchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: int) -> int:
|
||||
future = slow_computation(some_input)
|
||||
return future.result() # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Asynchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: int) -> int:
|
||||
return await slow_computation(some_input) # Await result asynchronously
|
||||
```
|
||||
|
||||
## When to use a task
|
||||
|
||||
**Tasks** are useful in the following scenarios:
|
||||
|
||||
- **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow.
|
||||
- **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details.
|
||||
- **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs).
|
||||
- **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/).
|
||||
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
|
||||
|
||||
## Serialization
|
||||
|
||||
There are two key aspects to serialization in LangGraph:
|
||||
|
||||
1. `@entrypoint` inputs and outputs must be JSON-serializable.
|
||||
2. `@task` outputs must be JSON-serializable.
|
||||
|
||||
These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives
|
||||
like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable.
|
||||
|
||||
Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution.
|
||||
|
||||
Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer.
|
||||
|
||||
## Determinism
|
||||
|
||||
To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic.
|
||||
|
||||
LangGraph achieves this behavior by persisting **task** and [**subgraph**](./low_level.md#subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same
|
||||
|
||||
While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
|
||||
|
||||
## Functional API vs. Graph API
|
||||
|
||||
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences:
|
||||
|
||||
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
|
||||
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
|
||||
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
|
||||
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Handling side effects
|
||||
|
||||
Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to ensure they are not executed multiple times when resuming a workflow.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, a side effect (writing to a file) is directly included in the workflow, so it will be executed a second time when resuming the workflow.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# This code will be executed a second time when resuming the workflow.
|
||||
# Which is likely not what you want.
|
||||
# highlight-next-line
|
||||
with open("output.txt", "w") as f:
|
||||
# highlight-next-line
|
||||
f.write("Side effect executed")
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def write_to_file():
|
||||
with open("output.txt", "w") as f:
|
||||
f.write("Side effect executed")
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# The side effect is now encapsulated in a task.
|
||||
write_to_file().result()
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
### Non-deterministic control flow
|
||||
|
||||
Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned.
|
||||
|
||||
* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ...
|
||||
* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ...
|
||||
|
||||
This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list
|
||||
of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value.
|
||||
This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts.
|
||||
|
||||
If order of execution is not maintained when resuming, one `interrupt` call may be matched with the wrong `resume` value, leading to incorrect results.
|
||||
|
||||
Please read the section on [determinism](#determinism) for more details.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = time.time()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input.
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def get_time() -> float:
|
||||
return time.time()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = get_time().result()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
Below are a few simple patterns that show examples of **how to** use the **Functional API**.
|
||||
|
||||
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = inputs["value"]
|
||||
another_value = inputs["another_value"]
|
||||
...
|
||||
|
||||
my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
```
|
||||
|
||||
### Parallel execution
|
||||
|
||||
Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs).
|
||||
|
||||
```python
|
||||
@task
|
||||
def add_one(number: int) -> int:
|
||||
return number + 1
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(numbers: list[int]) -> list[str]:
|
||||
futures = [add_one(i) for i in numbers]
|
||||
return [f.result() for f in futures]
|
||||
```
|
||||
|
||||
### Calling subgraphs
|
||||
|
||||
The **Functional API** and the [**Graph API**](./low_level.md) can be used together in the same application as they share the same underlying runtime.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
builder = StateGraph()
|
||||
...
|
||||
some_graph = builder.compile()
|
||||
|
||||
@entrypoint()
|
||||
def some_workflow(some_input: dict) -> int:
|
||||
# Call a graph defined using the graph API
|
||||
result_1 = some_graph.invoke(...)
|
||||
# Call another graph defined using the graph API
|
||||
result_2 = another_graph.invoke(...)
|
||||
return {
|
||||
"result_1": result_1,
|
||||
"result_2": result_2
|
||||
}
|
||||
```
|
||||
|
||||
### Calling other entrypoints
|
||||
|
||||
You can call other **entrypoints** from within an **entrypoint** or a **task**.
|
||||
|
||||
```python
|
||||
@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
|
||||
def some_other_workflow(inputs: dict) -> int:
|
||||
return inputs["value"]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = some_other_workflow.invoke({"value": 1})
|
||||
return value
|
||||
```
|
||||
|
||||
### Streaming custom data
|
||||
|
||||
You can stream custom data from an **entrypoint** by using the `StreamWriter` type. This allows you to write custom data to the `custom` stream.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
@task
|
||||
def add_one(x):
|
||||
return x + 1
|
||||
|
||||
@task
|
||||
def add_two(x):
|
||||
return x + 2
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter) -> int:
|
||||
"""A simple workflow that adds one and two to a number."""
|
||||
writer("hello") # Write some data to the `custom` stream
|
||||
add_one(inputs['number']).result() # Will write data to the `updates` stream
|
||||
writer("world") # Write some more data to the `custom` stream
|
||||
add_two(inputs['number']).result() # Will write data to the `updates` stream
|
||||
return 5
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
('updates', {'add_one': 2})
|
||||
('updates', {'add_two': 3})
|
||||
('custom', 'hello')
|
||||
('custom', 'world')
|
||||
('updates', {'main': 5})
|
||||
```
|
||||
|
||||
!!! important
|
||||
|
||||
The `writer` parameter is automatically injected at run time. It will only be injected if the
|
||||
parameter name appears in the function signature with that *exact* name.
|
||||
|
||||
|
||||
### Retry policy
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
attempts = 0
|
||||
|
||||
# Let's configure the RetryPolicy to retry on ValueError.
|
||||
# The default RetryPolicy is optimized for retrying specific network errors.
|
||||
retry_policy = RetryPolicy(retry_on=ValueError)
|
||||
|
||||
@task(retry=retry_policy)
|
||||
def get_info():
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError('Failure')
|
||||
return "OK"
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer):
|
||||
return get_info().result()
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'OK'
|
||||
```
|
||||
|
||||
### Resuming after an error
|
||||
|
||||
```python
|
||||
import time
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
# This variable is just used for demonstration purposes to simulate a network failure.
|
||||
# It's not something you will have in your actual code.
|
||||
attempts = 0
|
||||
|
||||
@task()
|
||||
def get_info():
|
||||
"""
|
||||
Simulates a task that fails once before succeeding.
|
||||
Raises an exception on the first attempt, then returns "OK" on subsequent tries.
|
||||
"""
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError("Failure") # Simulate a failure on the first attempt
|
||||
return "OK"
|
||||
|
||||
# Initialize an in-memory checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@task
|
||||
def slow_task():
|
||||
"""
|
||||
Simulates a slow-running task by introducing a 1-second delay.
|
||||
"""
|
||||
time.sleep(1)
|
||||
return "Ran slow task."
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter):
|
||||
"""
|
||||
Main workflow function that runs the slow_task and get_info tasks sequentially.
|
||||
|
||||
Parameters:
|
||||
- inputs: Dictionary containing workflow input values.
|
||||
- writer: StreamWriter for streaming custom data.
|
||||
|
||||
The workflow first executes `slow_task` and then attempts to execute `get_info`,
|
||||
which will fail on the first invocation.
|
||||
"""
|
||||
slow_task_result = slow_task().result() # Blocking call to slow_task
|
||||
get_info().result() # Exception will be raised here on the first attempt
|
||||
return slow_task_result
|
||||
|
||||
# Workflow execution configuration with a unique thread identifier
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1" # Unique identifier to track workflow execution
|
||||
}
|
||||
}
|
||||
|
||||
# This invocation will take ~1 second due to the slow_task execution
|
||||
try:
|
||||
# First invocation will raise an exception due to the `get_info` task failing
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
except ValueError:
|
||||
pass # Handle the failure gracefully
|
||||
```
|
||||
|
||||
When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint.
|
||||
|
||||
```python
|
||||
main.invoke(None, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'Ran slow task.'
|
||||
```
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
The functional API supports [human-in-the-loop](human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive.
|
||||
|
||||
Please see the following examples for more details:
|
||||
|
||||
* [How to wait for user input (Functional API)](../how-tos/wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API.
|
||||
* [How to review tool calls (Functional API)](../how-tos/review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API.
|
||||
|
||||
### Short-term memory
|
||||
|
||||
[State management](#state-management) using the **previous** parameter and optionally using the `entrypoint.final` primitive can be used to implement [short term memory](memory.md).
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add thread-level persistence (functional API)](../how-tos/persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Long-term memory
|
||||
|
||||
[long-term memory](memory.md#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information
|
||||
about a given user in one conversation and using it in another.
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add cross-thread persistence (functional API)](../how-tos/cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Workflows
|
||||
|
||||
* [Workflows and agent](../tutorials/workflows/index.md) guide for more examples of how to build workflows using the Functional API.
|
||||
|
||||
### Agents
|
||||
|
||||
* [How to create a React agent from scratch (Functional API)](../how-tos/react-agent-from-scratch-functional.ipynb): Shows how to create a simple React agent from scratch using the functional API.
|
||||
* [How to build a multi-agent network](../how-tos/multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API.
|
||||
* [How to add multi-turn conversation in a multi-agent application (functional API)](../how-tos/multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents.
|
||||
|
||||
@@ -1,58 +1,26 @@
|
||||
# Why LangGraph?
|
||||
|
||||
LLMs are extremely powerful, particularly when connected to other systems such as a retriever or APIs. This is why many LLM applications use a control flow of steps before and / or after LLM calls. As an example [RAG](https://github.com/langchain-ai/rag-from-scratch) performs retrieval of relevant documents to a question, and passes those documents to an LLM in order to ground the response. Often a control flow of steps before and / or after an LLM is called a "chain." Chains are a popular paradigm for programming with LLMs and offer a high degree of reliability; the same set of steps runs with each chain invocation.
|
||||
## LLM applications
|
||||
|
||||
However, we often want LLM systems that can pick their own control flow! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): an agent is a system that uses an LLM to decide the control flow of an application. Unlike a chain, an agent gives an LLM some degree of control over the sequence of steps in the application. Examples of using an LLM to decide the control of an application:
|
||||
LLMs make it possible to embed intelligence into a new class of applications. There are many patterns for building applications that use LLMs. [Workflows](https://www.anthropic.com/research/building-effective-agents) have scaffolding of predefined code paths around LLM calls. LLMs can direct the control flow through these predefined code paths, which some consider to be an "[agentic system](https://www.anthropic.com/research/building-effective-agents)". In other cases, it's possible to remove this scaffolding, creating autonomous agents that can [plan](https://huyenchip.com/2025/01/07/agents.html), take actions via [tool calls](https://python.langchain.com/docs/concepts/tool_calling/), and directly respond [to the feedback from their own actions](https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/) with further actions.
|
||||
|
||||
- Using an LLM to route between two potential paths
|
||||
- Using an LLM to decide which of many tools to call
|
||||
- Using an LLM to decide whether the generated answer is sufficient or more work is need
|
||||

|
||||
|
||||
There are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/) to consider, which give an LLM varying levels of control. On one extreme, a router allows an LLM to select a single step from a specified set of options and, on the other extreme, a fully autonomous long-running agent may have complete freedom to select any sequence of steps that it wants for a given problem.
|
||||
## What LangGraph provides
|
||||
|
||||

|
||||
LangGraph provides low-level supporting infrastructure that sits underneath *any* workflow or agent. It does not abstract prompts or architecture, and provides three central benefits:
|
||||
|
||||
Several concepts are utilized in many agent architectures:
|
||||
### Persistence
|
||||
|
||||
- [Tool calling](agentic_concepts.md#tool-calling): this is often how LLMs make decisions
|
||||
- Action taking: often times, the LLMs' outputs are used as the input to an action
|
||||
- [Memory](agentic_concepts.md#memory): reliable systems need to have knowledge of things that occurred
|
||||
- [Planning](agentic_concepts.md#planning): planning steps (either explicit or implicit) are useful for ensuring that the LLM, when making decisions, makes them in the highest fidelity way.
|
||||
LangGraph has a [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), which offers a number of benefits:
|
||||
|
||||
## Challenges
|
||||
- [Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): LangGraph persists arbitrary aspects of your application's state, supporting memory of conversations and other updates within and across user interactions;
|
||||
- [Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Because state is checkpointed, execution can be interrupted and resumed, allowing for decisions, validation, and corrections via human input.
|
||||
|
||||
In practice, there is often a trade-off between control and reliability. As we give LLMs more control, the application often become less reliable. This can be due to factors such as LLM non-determinism and / or errors in selecting tools (or steps) that the agent uses (takes).
|
||||
### Streaming
|
||||
|
||||

|
||||
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/streaming.ipynb#updates)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
|
||||
|
||||
## Core Principles
|
||||
### Debugging and Deployment
|
||||
|
||||
The motivation of LangGraph is to help bend the curve, preserving higher reliability as we give the agent more control over the application. We'll outline a few specific pillars of LangGraph that make it well suited for building reliable agents.
|
||||
|
||||

|
||||
|
||||
**Controllability**
|
||||
|
||||
LangGraph gives the developer a high degree of [control](../how-tos/index.md#controllability) by expressing the flow of the application as a set of nodes and edges. All nodes can access and modify a common state (memory). The control flow of the application can set using edges that connect nodes, either deterministically or via conditional logic.
|
||||
|
||||
**Persistence**
|
||||
|
||||
LangGraph gives the developer many options for [persisting](../how-tos/index.md#persistence) graph state using short-term or long-term (e.g., via a database) memory.
|
||||
|
||||
**Human-in-the-Loop**
|
||||
|
||||
The persistence layer enables several different [human-in-the-loop](../how-tos/index.md#human-in-the-loop) interaction patterns with agents; for example, it's possible to pause an agent, review its state, edit it state, and approve a follow-up step.
|
||||
|
||||
**Streaming**
|
||||
|
||||
LangGraph comes with first class support for [streaming](../how-tos/index.md#streaming), which can expose state to the user (or developer) over the course of agent execution. LangGraph supports streaming of both events ([like a tool call being taken](../how-tos/stream-updates.ipynb)) as well as of [tokens that an LLM may emit](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
## Debugging
|
||||
|
||||
Once you've built a graph, you often want to test and debug it. [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file) is a specialized IDE for visualization and debugging of LangGraph applications.
|
||||
|
||||

|
||||
|
||||
## Deployment
|
||||
|
||||
Once you have confidence in your LangGraph application, many developers want an easy path to deployment. [LangGraph Platform](../concepts/index.md#langgraph-platform) offers a range of options for deploying LangGraph graphs.
|
||||
LangGraph provides an easy onramp for testing, debugging, and deploying applications via [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). This includes [Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/), an IDE that enables visualization, interaction, and debugging of workflows or agents. This also includes numerous [options](https://langchain-ai.github.io/langgraph/tutorials/deployment/) for deployment.
|
||||
@@ -647,19 +647,15 @@ def node_in_parent_graph(state: State):
|
||||
This will print out
|
||||
|
||||
```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'),)}
|
||||
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)}
|
||||
--- 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}
|
||||
{'parent_node': {'state_counter': 1}}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user