mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 13:45:44 +02:00
Compare commits
96
Commits
cli==0.2.8
...
cli==0.2.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909a4591a8 | ||
|
|
fc1ef29df7 | ||
|
|
30a72ced06 | ||
|
|
302ae69e60 | ||
|
|
6777b5274c | ||
|
|
c5283cac09 | ||
|
|
562d64bbb7 | ||
|
|
898f266f72 | ||
|
|
331d5b07ce | ||
|
|
62069439fc | ||
|
|
0d9b664360 | ||
|
|
e8cea06e7e | ||
|
|
ed7f038a19 | ||
|
|
e507f0b991 | ||
|
|
af961e279b | ||
|
|
dd55e5097b | ||
|
|
92cc3f0e0e | ||
|
|
4210188ade | ||
|
|
6fc1df9013 | ||
|
|
7850c8d799 | ||
|
|
83d2f93566 | ||
|
|
c937d5f048 | ||
|
|
1a6395fd07 | ||
|
|
6e0041529e | ||
|
|
d5d6fc0fee | ||
|
|
31b135f75d | ||
|
|
bebb0e8164 | ||
|
|
4761eb7696 | ||
|
|
f1c1eaf229 | ||
|
|
0211886bf5 | ||
|
|
0dd9fba0af | ||
|
|
f5bf77b3eb | ||
|
|
7c9f9aa89d | ||
|
|
d38303494c | ||
|
|
c4deb2c621 | ||
|
|
42d88a769a | ||
|
|
14b07d06fa | ||
|
|
d04570f178 | ||
|
|
6c155f87c3 | ||
|
|
0264363083 | ||
|
|
1edf5cee89 | ||
|
|
0e81699fec | ||
|
|
a2a1a42c75 | ||
|
|
6b78bcd857 | ||
|
|
d33c5a20e4 | ||
|
|
09fdc14d0a | ||
|
|
64491a2b29 | ||
|
|
057da43cd0 | ||
|
|
a11a62e68f | ||
|
|
1d977f1c09 | ||
|
|
5cff35d1c3 | ||
|
|
ed3f05260a | ||
|
|
b3ea406e81 | ||
|
|
2a1c63ff9c | ||
|
|
1aecde3cd8 | ||
|
|
a446f34ed9 | ||
|
|
c2776449fd | ||
|
|
88fd6b1e80 | ||
|
|
10b1676d0b | ||
|
|
c1bf678ed1 | ||
|
|
9938b51d31 | ||
|
|
b84ae660b8 | ||
|
|
e3146d8050 | ||
|
|
d56c3b0f26 | ||
|
|
cf89499507 | ||
|
|
79dffe20b2 | ||
|
|
a9ea0cd28a | ||
|
|
ffaddab110 | ||
|
|
4aeaffef4e | ||
|
|
b44be763fb | ||
|
|
00d2a1abd4 | ||
|
|
2e8e9e4531 | ||
|
|
0e2471b401 | ||
|
|
e401ad7c75 | ||
|
|
f1bfd6051a | ||
|
|
998be75f34 | ||
|
|
e51442279a | ||
|
|
365dd5f459 | ||
|
|
8837f8452c | ||
|
|
306915690c | ||
|
|
0e61cc2cf6 | ||
|
|
a0a302dec5 | ||
|
|
b19405b296 | ||
|
|
101ad12292 | ||
|
|
df58a71567 | ||
|
|
ee04c32bb3 | ||
|
|
4e05db8537 | ||
|
|
d6e20e6d09 | ||
|
|
dd4ad48864 | ||
|
|
f79c8487d9 | ||
|
|
284e9a2cb4 | ||
|
|
9216e949e9 | ||
|
|
a6e2d9e197 | ||
|
|
abae398a3a | ||
|
|
78f809bc52 | ||
|
|
4b7ec256e9 |
@@ -180,3 +180,4 @@ Chinook.db
|
||||
|
||||
.vercel
|
||||
.turbo
|
||||
.editorconfig
|
||||
|
||||
@@ -277,7 +277,7 @@ def update_markdown_with_imports(markdown: str, path: str) -> str:
|
||||
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
|
||||
)
|
||||
# Return the code block with prepended API reference links
|
||||
return f"{indent}API Reference: {api_links}\n\n{original_code_block}"
|
||||
return f"{indent}<sup><i>API Reference: {api_links}</i></sup>\n\n{original_code_block}"
|
||||
|
||||
# Apply the replace_code_block function to all matches in the markdown
|
||||
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
|
||||
|
||||
@@ -220,6 +220,24 @@ def _convert_links_in_markdown(markdown: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
class HideCellTagPreprocessor(Preprocessor):
|
||||
"""
|
||||
Removes cells that have '# hide-cell' at the beginning of the cell content.
|
||||
This allows authors to include cells in the notebook that should not
|
||||
appear in the generated markdown output.
|
||||
"""
|
||||
|
||||
def preprocess(self, nb, resources):
|
||||
# Filter out cells with the '# hide-cell' comment at the beginning
|
||||
nb.cells = [
|
||||
cell
|
||||
for cell in nb.cells
|
||||
if not (cell.source.strip().startswith("# hide-cell"))
|
||||
]
|
||||
|
||||
return nb, resources
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
@@ -341,6 +359,7 @@ class ExtractAttachmentsPreprocessor(Preprocessor):
|
||||
|
||||
exporter = MarkdownExporter(
|
||||
preprocessors=[
|
||||
HideCellTagPreprocessor,
|
||||
EscapePreprocessor,
|
||||
ExtractAttachmentsPreprocessor,
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
eNrVVk1vHMcRRW75GY1FAMXGzn7MUiQlwTAokv6KRAogg1iQjUXvTM1MhzPd4+4erjaOAEUJDPi4yCm3IGLEQPDXyUEO9jmH/AH5qLN/RF717JJcSrLhowEC3Omurnqv6lV1Pzo9JuuU0b/4TGlPViYeH27+6NTSRw05/5cnFfnCpCd39g8OHzdWfVN4X7vr/b6sVc/LY1XOeomp+o6kTYqTiUlnz5593MFhO+tcF51pYYRywhckKjkzVphM7N3dfrPTFZ1K3h9bck3pHUxHWGm9jFOqfcGnJ9KphE2VTsompbHUbkoWW5ksHV3YsHI6Tgw4aP+SXVXJnNyrNhDPJVbVgfsFI29qRAeMnDRyU3YenBYkU2Tsk6e/dWSjLWz4+df1DCnS0SJlrh/3Rvj7citJwCPa1YlJlc7nn+d/UHVXpJSV0tOTdnv++PX+60+3jdYUcj9/ekRUR7JUx/TZdksoOpzVNP9K1nWpEslW/d87o79Ybt8infti/jgebvwL+azBgv78xHnpG/foBNHpf/89rcg5cP3H/m+WJD492QGO+TeHRdMVg6G4LWciHsRXRTy6Hg+vD0bi7duHl4OcxPHm2pMDshDO/J86V/r+K9D/58BblQC8RdFqY310QAkU5GfzZ79C6SOgeWM0vDpaHwwG4oZYlOSgmeyYSirtfoT+vxfljtzCbVQbGMD7t0ixhKoiZxNxRRtNV26ItsLtkqMyw1LS4l5ZU1W+8u38rKSLK5AkRYi3/M6MraK2b84OsRKUpcjbxnlKIw8CLoIlLAIM2DR1blGGCEQZP53J58bX70cXmUf7rTDnp9o4rbLsy/ejt6yszjdOdnb37obW+xOqbqG17375/VkP/kALdjJTlmY6bupxiN3qXzdl2e0sO639WjbQvQ+7nbOWvfdxxytfEmLsZ5lKiD1zmNshzB/b/1ecuKkMvrZRoxCbpuKusUciYhi93BwDSWNLuFkOl+l02tOzhPf6JriOTBbBdRQY9CfK9GogwsGzpu+0UXchOrGVysqJQjrhWKlpQFWTqUtaQRAgwUpqQLmzI9pYtisO0DwkDkhLb/B50xpzVM60uGmsafJC3EESVIq4XZxNhTZTdsNRhsOBLxYZWKTjMvGeeIdELo9JHBvOmjdCilTxKCaIEt0T1IQTU1ijnJhIlSoVOTSJkGUpMhiLSYvFBQQocIm2ZmMOmam88Ox4wnIQE5kcrbJ+/vBvThCSZ6pZF4JNGwBRGhrk8FhSVW0NotTNBF0lnMyIlznWpFFlCsjQmtE5pwsTpfSFIisS5uoL6THnykBJsO4Z9DI+1nriZ1QsiJTTK4Xnhq0koLAbBsvVCwuvOgyBusRYtMigt7k+GozWRhvooIuXFTfYg+55K13ISSR+p45UTamSL7QI6d50udkzNu/zV58Pj8PhldYIPm+RNbOF519DPBopqz1VE5Rt2BXDa+uD1wJRLbYqwgn84JHqVaLwkznzsIN1GLS0LIDgAXOhfCu5PZ84K2V0mPbEt03cEx903sOYZIc7VEvrK2BGO1iMZicgQp6PKKxjOafW1CKjlK9jtIq1TZiBIsFUFjLna8NfCvWC0jCJ9vbEnZaZ+6CzUqSNtc342sb6j9TolkIYkArs3Iv0fnrh2OPYZOPWI/+CxzF7HLPHlWq+W9VUAj0ebKp7ru6lCFehbJtGJ6rsigkluDQEX1WYCG1dpgVpsX9lZzpDGdlPrlFA5HOryVETMQq6uDpoG2faVlYLKlEbblQTArY6CLNJuJqglrI14coUVAaPexgmQWobrcue2EfoxBtei7vQwmCzK26rpJBUipulMWydI7BmBmEsYKog3NQ0PH7cUQgeBk7SkmRAdB9ZamdIqSrlw/gJbLtnZkzGwMIuJYwhZU0l/BSEyFauZWaJWuIcuStcEQK7Jim4R9pAwNWGqS0dS84zAheq4pfAAqkjOkJyo7OUII0BUO9evP7hShrwBn5lBXFZcM7ja88f/jWOL1EF8oiRL8As4bdseuJdftxzr3AaA8HLksUtvazqQtRtiw43R2u7FzduSehiYbrLecyWkzAEURUnKMFjE2l//vDvPFni3QXzpaNzi9F7ptDicKUF16+tr21cjV/Wgu3rIzxwx15VMB/2NuIH/wfOR5bE
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
eNrtWQlYE2cahkpbPEvXVmmtMmbd4sGEmdwBIuWSyxDkkFPTycwfMpLMxJlJICBaQdsqtRqtV1etFQSlKiBey9ar1db1wJsKqO3Wtiq19fFYrXWp+yeAYnV3u7vu4T7M8xAm/3d/3/99//8+Ka60A46nWcZzHc0IgCNIAX7hFxRXcmCyDfDCjAoLEEwsVZ6oS04ps3F000iTIFj5oMBAwkqLWStgCFpMspZAOx5ImgghEL5bzcCtptzAUo7mHpsLRRbA80QO4EVBWYUikoWmGEEUJMpgbQjBAYRALIRgQiAHI4izmWwmNiE5JSk1IiVWl5AclM2gSBjP07yA6BLGZiB5NOR1CaAcMBMCoBCB4HN5N5sRBoE4WJs/1EqxDGhnhgtcO1MAwgHeyjJQhkUEE0B4mxVwdppnOYSiOUAKZodLUVIH1z2DLmYoazMLPMIa21XmsVxuALSDJOhSEJohzTYKIGEJGQgLuaFFkC+IRQEijjUDGC3v4AVgERUFdE1BHsyZP48MlyKjEPkIJB9R3hOw8YATFU0IEFlYCpjhQo5VQGViHHLwAgcIiyjISJh5ECASWNbcnlzBYXWJGm2Mu5SQ9e5rUKGIISwuKkFRkEABnuRoaztNFEbBnOSxCGOzGOCecPltJTjIDzPKu2StHKw2J9DA/Y1wfXQYaxeBgYkMD1mFy67NBJNLQQ+hoIsNBtXBxhomwaxDNldefpH3FlgD2mp2PBCCtoPwmMRB0XaaAg9EEele/i/HMKGo0gQICpqbW25iecG54f52ryZIEsDdCBiSpWgmx1mXU0BbYS8Ao6slA5ACXqCq4DZngDtsZ1UuAFaUMNN2UNEu66whrFYzTRIueuAknmXWdbQF6nLnQXKVqx9Q95BwbtJBV8JiAxMdcDoxCC5WysVYTT7KCwTNmOG0Qc0E9KrC6qb/vivBSpC5UAnaMfmcFe3CG7rysLxztZYgdcn3qSQ40uRcTXAWhayu6zpnYwTaApyVEYkPmusg3jMnFeO4WF17n2LewZDO1e5m3nqfMBA4B0qyUIfzfWxDZ37MgMkRTM4ytUy9pn2e8aCkAooJNr64HFYEHNxX2TFzV+niO0t51mNgeSSsjnN7iskWgGA4oiUciASTyBGJNEiCB+EYEq1NWRfRYSbloWWoTeEIhjfCUkR1Fr+SNNmYXEBVRTy04NtdBYfRuNyHow0F+VaWB2iHV8516WhS+2mDxkbWte8xlOVyCIYucJt1rnUVE54uNLOpgwwbwKUSGkctvLNMJpVt6KB05rkKxoWhOIZieH0+ysGwzbSFhrlzf3Ycb7DMOAafbQ9yCGwuYHjnGinW/uzoysIBC3TGZf2uonI1fD58OFOnLomLRy1V1N/PxoMuDpUpLPy2B+kdKlZh/Lr8TmaUppxNw+AXvRRXqwwGQoWTSjlFGBQSTK7ASRUJZJSSIEjZ7+B5QZNQi6t0VpYTUB6Q8DQXHM6mAAuR7+oqjRSXSxUw0uDOoyzZZohkXTHwwYgVnrUsQVWTRpQkSBNA23ebszIyIyFMGxtRlQydjGDZXBrMb/b01etJo95g0eAgRk9FZSQp7HYxrxOHq6V2rT4GU0dnahMBNlnPKuw6fXxB/litDMWVMgUuwxVKWDYxJsbFOJoZH28tiObotFyZNVeVUaDVa00CSZrGxnEFuanmyUl0fG4Bk5DI8OPInPFWmT4/ByfUkWOlRlO6zJrokEpVyZwOi5KOH2PM14qteo7CFJOS6Wi8QMDtpFUnEXKxKG0cjqtgiPBKoQkMRuD2hAOS13Q0CQqbBG1vEXlniwTDQ9+VGI34/rEYjMTA25GOMTuCkWRXhgH8D+d2Mi0ATQK8jzS9AxNjgwNeY7CPo6lx8WYtZTVJjQZsUgQu4aJy8goyk2MS2TCTKU8XNS5/rC4lJbdLZjC1AsU6kqPAZCr31rzn+j/p1ZZ0tGvPozr3WQSLy7A8QxuNFcnwjgQ4ZxVpZm0UnPAcqIgYgyaFZTg3qaVqKSZVEgDI1bjaoECj0pJqOrXdnRDlruOhkjDDjWcnnXUmqUYUJJNJRcHwIqdRKWQY5r4sTq9wbVQmZ69nm1+pt4f76QH/7tx5K1nLtmB9tt9O8w7xj7r0Q9k1S9hWYpD94mvEqSTf2ZHBrVTswq+IjHl3Jp5KMPtrp//4qveUKV+dK9v0jLfn7qH+u7acqGved6v19mE/v1HZIUW9miZsaXnnxmeLU69PT72p9+t7MPP1syGia/6NfY9vdl6YOV5zMCBpruUkVnoc63f8+oSnT7xbZqYXXrn2w8vLokuHLw380uHrtzQL9Q3a94fLr/mE3ziz8+Qa1U8lywKMlTFzGo72Guo339vzAzDIs3WZdcXIZ+qepTIdKevNJ2PUnh82bnw1ZX/t5Dvf+Z8/OnwkcFIL8/5w5ja4/sLOtmrRe6PpivE3F0wturj8otfCOYaSsP5T3jw/8JC3Z9S3U30bAk0zPDanJpddXU+dqp6+Yai+obEmI2LFS18EtPlkB4eOfakmXfH2/sWNias3Va7/qOG3b/ya821EbGDd2W29R74T3nMrGpD0mVYpV6t6Xhz2Cr8lGhv/2ScDJ97ZvYH29S9K9b3c5LVdUzyxcKT/FUH9+6TqLy6svTBAcxod0zTgVszgy76WbUf6I0sO/pR0aHNz25/DU9kRoU+89f6Vfb1JZ2/76CtLjLVFr1ev7ivOK979ftso3wETV1pu7q4tn3PJz3vb8mvpA1cnPp9JRt2MWyl7ctTHLfnOqIxeu4fmP+WqcQ+PoGOp1kAvD49HiT+8dnfjj38Ff3RlZ2xmc8DdezhMgd6ds3sihDtPhHvJBUr0JGH+q8iEhpdQkYtBL1fmCLw8MkwqiQXaghyJoJjMkkx40t+CLwSXY7NA41C7qDBbRGSLghBpAJItMrje5EUi1y32vlhVYuyepy7vujqp/wXedGOxbiz238RiZz18utHY/woaqyDdt11ns+fTj+Nt999wD/05Pi3HJbjyHwOoA/5PAapULX+MAKr0kQNUo0JhUCgwOWYwkGoZgD7jUoIySFQGhRRIjdi/H6A+AowDFCo58egwzhMeD2CcA0wL5vPht6Pe1GRNB6Pml2WVDhhRPGT2lgs9bMiRE3GG/WmxVWtyf/i8x9G3hwX38xnsZcjJsdsXn46a4VG9EtmV2bpUJpp6++tPt7Zc8n+vdFD9vs+ngaVtp+pvnJk2InTEkj/12e545Zv30tPGxv/6CF80Y2lUa22a9qos/tiwA+c959WHHz3+4rlz10fOGnhtry1Ed26YRpJ16IWg4fuq3/QJt//x88KU538qKfZa7uO88JLFZ/bQWT41mdefXdkw+fzaq9igsIqF0Sdbxi/36b8rF40bM9O3t9+OM1+F9Jvda9zsy+zVltCrEVebHFms3/cbLXmViw/Ebi+53ugVYas8ha3sOXpPaZ1hxmurJFbgH6I0FH7yG681lqBZ+2/uILY+Vf+6QZ71sbFv1I43FlwvEi5OCX2hZG89nxLy3jOpcYXNLX161IeKbhMDw5WftM4JLgs/9HyehXbWH/lSOHZ0gfNVz/oFoLg280V9QdugiKPDdo5e81F+08sXWr0Lvlu7ZA+7qK53AZi54mrNd38sxvdnrhvTmn/UlDZZ47kq9vLhM9nHRl8/nHrtxVvPvtpnzqdVsgGjGhKChxwPuMFeWb2J2L8bU22apv74GL+pbfTIdbR/v2qv042Od8Ch1JLG9CErep2ooGwb487fEh1GmqPnerTDnHGafp6yRwxznjzeDXO6Yc6jgTn/iVyY08KU0ZMtMXgSHIdGx6Q4hcMWx0qlfxdnPCwhqrsJUT4kIXLFL8zIX/WpG/h1A79u4NcN/LqB3y8EfjI13g38XMBPIpM+RsAPf/S/TKolBiMuxSiZksJwCaFSUbhSaVQCo0IqU8jIxwH4qXAloSAf4Y9by34O/ObrdKcxn72Xbz3XuEw5vGdT6KU5E02HUz17vr8vV7eRGdyy2b+393d50fZFWtPaE6XP/HSocKp0xWdv4Ym/2tJnz4oFF5Y37Jyyc+d2Z1roVL8NlxWZp6d+wOz4ceCHO/bc/rh/wKqPvp+6amXeyZk124/H7H/32d5zl2H1i74tSG38etKEYUeyZgXtD4p2FI37JnUcem1Vw4Efy09n/QbbtUhDvYXfeLdhydGYKa+j8aMvVnzqk/9GjWYg/tTZ1vweIV7PvzmoeK/XTPmMl/dO3PDNLjLYa0jJ0rBvX/jik/Qva2ufu3gl8u3zPs32hpYJP0T0Wi8ZeWnQ3JD0/n59qMHDCg76Fo491M/vasm1LXkn5rXNvPi7qvS3p2KH8lYt9h+zaNuKUkWp7tKLs/Ys7FdHT//CMJebn1D2dUmK/amNGcenvXHrYKE5e+n30y75I15B4U2quUmz5vbVgSHnbrR9cCnkq+d8n/ix5Fen5oX7hYYbDjyt+f7JGYtfOaVcLxlccP7ktpfW50ZmMa2GCevT5g2uU3aAspBmj5KXYT3+AirE0Wo=
|
||||
@@ -126,7 +126,7 @@ def add_human_in_the_loop(
|
||||
*,
|
||||
interrupt_config: HumanInterruptConfig = None,
|
||||
) -> BaseTool:
|
||||
"""Wrap a tool to support human-in-the-loop review."""
|
||||
"""Wrap a tool to support human-in-the-loop review."""
|
||||
if not isinstance(tool, BaseTool):
|
||||
tool = create_tool(tool)
|
||||
|
||||
@@ -235,4 +235,4 @@ for chunk in agent.stream(
|
||||
|
||||
## Additional resources
|
||||
|
||||
* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md)
|
||||
* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md)
|
||||
|
||||
@@ -282,7 +282,7 @@ def greet(
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_user_info, greet],
|
||||
tools=[update_user_info, greet],
|
||||
# highlight-next-line
|
||||
state_schema=CustomState
|
||||
)
|
||||
@@ -420,4 +420,4 @@ LangGraph also allows you to [search](https://langchain-ai.github.io/langgraph/h
|
||||
|
||||
## Additional resources
|
||||
|
||||
* [Memory in LangGraph](../concepts/memory.md)
|
||||
* [Memory in LangGraph](../concepts/memory.md)
|
||||
|
||||
@@ -12,19 +12,13 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
|
||||
|
||||
helm repo add kedacore https://kedacore.github.io/charts
|
||||
helm install keda kedacore/keda --namespace keda --create-namespace
|
||||
|
||||
1. Ingress Configuration (recommended)
|
||||
1. Install `Ingress Nginx` to serve as a reverse proxy for your deployment.
|
||||
|
||||
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
||||
helm repo update
|
||||
helm install ingress-nginx ingress-nginx/ingress-nginx
|
||||
|
||||
1. Provision a root domain that will suffix all domains for your workloads (e.g. `us.langgraph.app`).
|
||||
1. Provision wildcard certificates to terminate TLS for your deployments.
|
||||
1. Note: If this step is skipped, you will need to provision domains/certs for each of your deployments.
|
||||
|
||||
1. Ingress Configuration
|
||||
1. You must set up an ingress for your LangSmith instance. All agents will be deployed as Kubernetes services behind this ingress.
|
||||
1. You can use this guide to [set up an ingress](https://docs.smith.langchain.com/self_hosting/configuration/ingress) for your instance.
|
||||
1. You have slack space in your cluster for multiple deployments. `Cluster-Autoscaler` is recommended to automatically provision new nodes.
|
||||
1. A valid Dynamic PV provisioner or PVs available on your cluster. You can verify this by running:
|
||||
|
||||
kubectl get storageclass
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -44,13 +38,12 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "aa9dff4"
|
||||
|
||||
1. In your `values.yaml` file, enable the `langgraphPlatform` option.
|
||||
|
||||
1. In your `values.yaml` file, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
|
||||
config:
|
||||
langgraphPlatform:
|
||||
enabled: true
|
||||
langgraphPlatformLicenseKey: "YOUR_LANGGRAPH_PLATFORM_LICENSE_KEY"
|
||||
rootDomain: "YOUR_ROOT_DOMAIN"
|
||||
1. In your `values.yaml` file, configure the `hostBackendImage` and `operatorImage` options (if you need to mirror images)
|
||||
|
||||
1. You can also configure base templates for your agents by overriding the base templates [here](https://github.com/langchain-ai/helm/blob/main/charts/langsmith/values.yaml#L898).
|
||||
1. You create a deployment from the [Control Plane UI](../../concepts/langgraph_control_plane.md#control-plane-ui).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -91,6 +91,10 @@ Database Connectivity:
|
||||
|
||||
- The custom Postgres instance must be accessible by the LangGraph Server. The user is responsible for ensuring connectivity.
|
||||
|
||||
## `LANGGRAPH_POSTGRES_POOL_MAX_SIZE`
|
||||
|
||||
Beginning with langgraph-api version `0.2.12`, the maximum size of the Postgres connection pool can be controlled using the `LANGGRAPH_POSTGRES_POOL_MAX_SIZE` environment variable. By setting this variable, you can determine the upper bound on the number of simultaneous connections the server will establish with the Postgres database. This is particularly useful for deployments where database resources are limited (or more available) or where you need to tune connection behavior for performance or scaling reasons. If not specified, the pool size defaults to 150 connections.
|
||||
|
||||
## `REDIS_URI_CUSTOM`
|
||||
|
||||
!!! info "Only for Self-Hosted Data Plane and Self-Hosted Control Plane"
|
||||
|
||||
@@ -40,7 +40,7 @@ For more information, please see:
|
||||
|
||||
## Self-Hosted Data Plane
|
||||
|
||||
The [Self-Hosted Data Plane](./langgraph_self_hosted_data_plane.md) deployment option is a "hybrid" model for deployemnt where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us.
|
||||
The [Self-Hosted Data Plane](./langgraph_self_hosted_data_plane.md) deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us.
|
||||
|
||||
Build a Docker image using the [LangGraph CLI](./langgraph_cli.md) and deploy your LangGraph Server from the [Control Plane UI](./langgraph_control_plane.md#control-plane-ui).
|
||||
|
||||
|
||||
@@ -400,24 +400,14 @@ When using the `interrupt` function, the graph will pause at the interrupt and w
|
||||
|
||||
Graph execution can be resumed using the [Command](../reference/types.md#langgraph.types.Command) primitive which can be passed through the `invoke`, `ainvoke`, `stream` or `astream` methods.
|
||||
|
||||
The `Command` primitive provides several options to control and modify the graph's state during resumption:
|
||||
The `Command` primitive provides a way to **pass a value** (such as user's input) to the `interrupt` via `Command(resume=value)`. Execution resumes from the beginning of the node where the `interrupt` was used, however, this time the `interrupt(...)` call will return the value passed in the `Command(resume=value)` instead of pausing the graph.
|
||||
|
||||
1. **Pass a value to the `interrupt`**: Provide data, such as a user's response, to the graph using `Command(resume=value)`. Execution resumes from the beginning of the node where the `interrupt` was used, however, this time the `interrupt(...)` call will return the value passed in the `Command(resume=value)` instead of pausing the graph.
|
||||
```python
|
||||
# Resume graph execution with the user's input.
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
```python
|
||||
# Resume graph execution with the user's input.
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
2. **Update the graph state**: Modify the graph state using `Command(update=update)`. Note that resumption starts from the beginning of the node where the `interrupt` was used. Execution resumes from the beginning of the node where the `interrupt` was used, but with the updated state.
|
||||
|
||||
```python
|
||||
# Update the graph state and resume.
|
||||
# You must provide a `resume` value if using an `interrupt`.
|
||||
graph.invoke(Command(update={"foo": "bar"}, resume="Let's go!!!"), thread_config)
|
||||
```
|
||||
|
||||
By leveraging `Command`, you can resume graph execution, handle user inputs, and dynamically adjust the graph's state.
|
||||
By leveraging `Command`, you can resume graph execution and handle user inputs.
|
||||
|
||||
## How does resuming from an interrupt work?
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
- [Cron Jobs](./langgraph_server.md#cron-jobs): Cron jobs are a way to schedule tasks to run at specific times in your LangGraph application.
|
||||
- [Double Texting](./double_texting.md): Double texting is a common issue in LLM applications where users may send multiple messages before the graph has finished running. This guide explains how to handle double texting with LangGraph Deploy.
|
||||
- [Authentication & Access Control](./auth.md): Learn about options for authentication and access control when deploying the LangGraph Platform.
|
||||
- [MCP Endpoint](./server-mcp.md): Expose your LangGraph agents as MCP tools using an MCP endpoint.
|
||||
|
||||
### Deployment Options
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ To deploy a [LangGraph Server](../concepts/langgraph_server.md), follow the how-
|
||||
|
||||
## Overview
|
||||
|
||||
LangGraph Platform's Self-Hosted Data Plane deployment option is a "hybrid" model for deployemnt where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud.
|
||||
LangGraph Platform's Self-Hosted Data Plane deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud.
|
||||
|
||||
| | [Control Plane](../concepts/langgraph_control_plane.md) | [Data Plane](../concepts/langgraph_data_plane.md) |
|
||||
|-------------------|-------------------|------------|
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
tags:
|
||||
- mcp
|
||||
- platform
|
||||
hide:
|
||||
- tags
|
||||
---
|
||||
|
||||
# MCP Endpoint
|
||||
|
||||
The **Model Context Protocol (MCP)** is an open protocol for describing tools and data sources in a model-agnostic format, enabling LLMs to discover
|
||||
and use them via a structured API.
|
||||
|
||||
[LangGraph Server](./langgraph_server.md) implements MCP using the [Streamable HTTP transport](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/#streamable-http). This allows LangGraph **agents** to be exposed as **MCP tools**, making them usable with any MCP-compliant client supporting Streamable HTTP.
|
||||
|
||||
The MCP endpoint is available at:
|
||||
|
||||
```
|
||||
/mcp
|
||||
```
|
||||
|
||||
on [LangGraph Server](./langgraph_server.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
To use MCP, ensure you have the following dependencies installed:
|
||||
|
||||
- `langgraph-api >= 0.2.3`
|
||||
- `langgraph-sdk >= 0.1.61`
|
||||
|
||||
Install them with:
|
||||
|
||||
```bash
|
||||
pip install "langgraph-api>=0.2.3" "langgraph-sdk>=0.1.61"
|
||||
```
|
||||
|
||||
## Exposing an agent as MCP tool
|
||||
|
||||
|
||||
When deployed, your agent will appear as a tool in the MCP endpoint
|
||||
with this configuration:
|
||||
|
||||
- **Tool name**: The agent's name.
|
||||
- **Tool description**: The agent's description.
|
||||
- **Tool input schema**: The agent's input schema.
|
||||
|
||||
### Setting name and description
|
||||
|
||||
You can set the name and description of your agent in `langgraph.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"graphs": {
|
||||
"my_agent": {
|
||||
"path": "./my_agent/agent.py:graph",
|
||||
"description": "A description of what the agent does"
|
||||
}
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
After deployment, you can update the name and description using the LangGraph SDK.
|
||||
|
||||
### Schema
|
||||
|
||||
Define clear, minimal input and output schemas to avoid exposing unnecessary internal complexity to the LLM.
|
||||
|
||||
The default [MessagesState](./low_level.md#messagesstate) uses `AnyMessage`, which supports many message types but is too general for direct LLM exposure.
|
||||
|
||||
Instead, define **custom agents or workflows** that use explicitly typed input and output structures.
|
||||
|
||||
For example, a workflow answering documentation questions might look like this:
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
# Define input schema
|
||||
class InputState(TypedDict):
|
||||
question: str
|
||||
|
||||
# Define output schema
|
||||
class OutputState(TypedDict):
|
||||
answer: str
|
||||
|
||||
# Combine input and output
|
||||
class OverallState(InputState, OutputState):
|
||||
pass
|
||||
|
||||
# Define the processing node
|
||||
def answer_node(state: InputState):
|
||||
# Replace with actual logic and do something useful
|
||||
return {"answer": "bye", "question": state["question"]}
|
||||
|
||||
# Build the graph with explicit schemas
|
||||
builder = StateGraph(OverallState, input=InputState, output=OutputState)
|
||||
builder.add_node(answer_node)
|
||||
builder.add_edge(START, "answer_node")
|
||||
builder.add_edge("answer_node", END)
|
||||
graph = builder.compile()
|
||||
|
||||
# Run the graph
|
||||
print(graph.invoke({"question": "hi"}))
|
||||
```
|
||||
|
||||
For more details, see the [low-level concepts guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#state).
|
||||
|
||||
|
||||
## Usage overview
|
||||
|
||||
To enable MCP:
|
||||
|
||||
- Upgrade to use langgraph-api>=0.2.3. If you are deploying LangGraph Platform, this will be done for you automatically if you create a new revision.
|
||||
- MCP tools (agents) will be automatically exposed.
|
||||
- Connect with any MCP-compliant client that supports Streamable HTTP.
|
||||
|
||||
|
||||
### Client
|
||||
|
||||
Use an MCP-compliant client to connect to the LangGraph server. The following examples show how to connect using different programming languages.
|
||||
|
||||
=== "JavaScript/TypeScript"
|
||||
|
||||
```bash
|
||||
npm install @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> Replace `serverUrl` with your LangGraph server URL and configure authentication headers as needed.
|
||||
|
||||
```js
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
|
||||
// Connects to the LangGraph MCP endpoint
|
||||
async function connectClient(url) {
|
||||
const baseUrl = new URL(url);
|
||||
const client = new Client({
|
||||
name: 'streamable-http-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(baseUrl);
|
||||
await client.connect(transport);
|
||||
|
||||
console.log("Connected using Streamable HTTP transport");
|
||||
console.log(JSON.stringify(await client.listTools(), null, 2));
|
||||
return client;
|
||||
}
|
||||
|
||||
const serverUrl = "http://localhost:2024/mcp";
|
||||
|
||||
connectClient(serverUrl)
|
||||
.then(() => {
|
||||
console.log("Client connected successfully");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Failed to connect client:", error);
|
||||
});
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
No official MCP client is available for Python yet.
|
||||
|
||||
|
||||
## Session behavior
|
||||
|
||||
The current LangGraph MCP implementation does not support sessions. Each `/mcp` request is stateless and independent.
|
||||
|
||||
## Authentication
|
||||
|
||||
The `/mcp` endpoint uses the same authentication as the rest of the LangGraph API. Refer to the [authentication guide](./auth.md) for setup details.
|
||||
|
||||
## Disabling MCP
|
||||
|
||||
To disable the MCP endpoint, set `disable_mcp` to `true` in your `langgraph.json` configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"disable_mcp": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will prevent the server from exposing the `/mcp` endpoint.
|
||||
@@ -112,7 +112,7 @@ Assuming you are using JWT token authentication, you could access your deploymen
|
||||
const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider
|
||||
const client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
headers: { Authorization: `Bearer ${my_token}` },
|
||||
defaultHeaders: { Authorization: `Bearer ${my_token}` },
|
||||
});
|
||||
const threads = await client.threads.search();
|
||||
```
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,12 +30,6 @@ Explore practical implementations tailored for specific scenarios:
|
||||
### RAG
|
||||
|
||||
- [Agentic RAG](rag/langgraph_agentic_rag.ipynb): Use an agent to figure out how to retrieve the most relevant information before using the retrieved information to answer the user's question.
|
||||
- [Adaptive RAG](rag/langgraph_adaptive_rag.ipynb): Adaptive RAG is a strategy for RAG that unites (1) query analysis with (2) active / self-corrective RAG. Implementation of: https://arxiv.org/abs/2403.14403
|
||||
- For a version that uses a local LLM: [Adaptive RAG using local LLMs](rag/langgraph_adaptive_rag_local.ipynb)
|
||||
- [Corrective RAG](rag/langgraph_crag.ipynb): Uses an LLM to grade the quality of the retrieved information from the given source, and if the quality is low, it will try to retrieve the information from another source. Implementation of: https://arxiv.org/pdf/2401.15884.pdf
|
||||
- For a version that uses a local LLM: [Corrective RAG using local LLMs](rag/langgraph_crag_local.ipynb)
|
||||
- [Self-RAG](rag/langgraph_self_rag.ipynb): Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. Implementation of https://arxiv.org/abs/2310.11511.
|
||||
- For a version that uses a local LLM: [Self-RAG using local LLMs](rag/langgraph_self_rag_local.ipynb)
|
||||
- [SQL Agent](sql-agent.ipynb): Build a SQL agent that can answer questions about a SQL database.
|
||||
|
||||
|
||||
@@ -45,7 +39,6 @@ Explore practical implementations tailored for specific scenarios:
|
||||
|
||||
- [Network](multi_agent/multi-agent-collaboration.ipynb): Enable two or more agents to collaborate on a task
|
||||
- [Supervisor](multi_agent/agent_supervisor.ipynb): Use an LLM to orchestrate and delegate to individual agents
|
||||
- [Hierarchical Teams](multi_agent/hierarchical_agent_teams.ipynb): Orchestrate nested teams of agents to solve problems
|
||||
|
||||
#### Planning Agents
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -749,7 +749,7 @@
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -529,7 +529,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"workflow.add_node(\"web_search_node\", web_search) # web search\n",
|
||||
"\n",
|
||||
|
||||
@@ -478,7 +478,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -626,7 +626,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -614,7 +614,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
+1
-5
@@ -175,11 +175,6 @@ nav:
|
||||
- how-tos/autogen-integration.ipynb
|
||||
- how-tos/autogen-integration-functional.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
- how-tos/create-react-agent-structured-output.ipynb
|
||||
- how-tos/create-react-agent-manage-message-history.ipynb
|
||||
- how-tos/react-agent-from-scratch.ipynb
|
||||
- how-tos/react-agent-from-scratch-functional.ipynb
|
||||
@@ -294,6 +289,7 @@ nav:
|
||||
- concepts/assistants.md
|
||||
- concepts/double_texting.md
|
||||
- concepts/auth.md
|
||||
- concepts/server-mcp.md
|
||||
- Deployment Options:
|
||||
- concepts/langgraph_cloud.md
|
||||
- concepts/langgraph_self_hosted_data_plane.md
|
||||
|
||||
@@ -216,5 +216,5 @@
|
||||
|
||||
|
||||
{% block announce %}
|
||||
<b>Join us at <a href="https://interrupt.langchain.com/" target="_blank" rel="noopener noreferrer"> Interrupt: The Agent AI Conference by LangChain</a> on May 13 & 14 in San Francisco!</b>
|
||||
<strong>We are growing and hiring for multiple roles for LangChain, LangGraph and LangSmith. <a href="https://www.langchain.com/careers" target="_blank" rel="noopener noreferrer"> Join our team!</a></strong>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
{% if not page.meta.hide_comments %}
|
||||
<h2 id="__comments">{{ lang.t("meta.comments") }}</h2>
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="langchain-ai/langgraph"
|
||||
data-repo-id="R_kgDOKFU0lQ"
|
||||
data-category="Discussions"
|
||||
data-category-id="DIC_kwDOKFU0lc4CfZgA"
|
||||
data-mapping="pathname"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="bottom"
|
||||
data-theme="preferred_color_scheme"
|
||||
data-lang="en"
|
||||
data-loading="lazy"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
|
||||
<!-- Synchronize Giscus theme with palette -->
|
||||
<script>
|
||||
var giscus = document.querySelector("script[src*=giscus]")
|
||||
|
||||
// Set palette on initial load
|
||||
var palette = __md_get("__palette")
|
||||
if (palette && typeof palette.color === "object") {
|
||||
var theme = palette.color.scheme === "slate"
|
||||
? "transparent_dark"
|
||||
: "light"
|
||||
|
||||
// Instruct Giscus to set theme
|
||||
giscus.setAttribute("data-theme", theme)
|
||||
}
|
||||
|
||||
// Register event handlers after documented loaded
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var ref = document.querySelector("[data-md-component=palette]")
|
||||
ref.addEventListener("change", function() {
|
||||
var palette = __md_get("__palette")
|
||||
if (palette && typeof palette.color === "object") {
|
||||
var theme = palette.color.scheme === "slate"
|
||||
? "transparent_dark"
|
||||
: "light"
|
||||
|
||||
// Instruct Giscus to change theme
|
||||
var frame = document.querySelector(".giscus-frame")
|
||||
frame.contentWindow.postMessage(
|
||||
{ giscus: { setConfig: { theme } } },
|
||||
"https://giscus.app"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -14,7 +14,7 @@ from langgraph.types import interrupt
|
||||
"""
|
||||
|
||||
EXPECTED_MARKDOWN = """\
|
||||
API Reference: <a href="https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt">interrupt</a>
|
||||
<sup><i>API Reference: <a href="https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt">interrupt</a></i></sup>
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
@@ -737,7 +737,7 @@
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -713,7 +713,7 @@
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -515,7 +515,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"workflow.add_node(\"web_search_node\", web_search) # web search\n",
|
||||
"\n",
|
||||
|
||||
@@ -486,7 +486,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -620,7 +620,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -614,7 +614,7 @@
|
||||
"# Define the nodes\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
|
||||
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
|
||||
"workflow.add_node(\"generate\", generate) # generatae\n",
|
||||
"workflow.add_node(\"generate\", generate) # generate\n",
|
||||
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
"id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
|
||||
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generate\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import sqlite3
|
||||
import threading
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
|
||||
class SqliteCache(BaseCache[ValueT]):
|
||||
"""File-based cache using SQLite."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
path: str,
|
||||
serde: SerializerProtocol | None = None,
|
||||
) -> None:
|
||||
"""Initialize the cache with a file path."""
|
||||
super().__init__(serde=serde)
|
||||
# SQLite backing store
|
||||
self._conn = sqlite3.connect(
|
||||
path,
|
||||
check_same_thread=False,
|
||||
)
|
||||
# Serialize access to the shared connection across threads
|
||||
self._lock = threading.RLock()
|
||||
# Better concurrency & atomicity
|
||||
self._conn.execute("PRAGMA journal_mode=WAL;")
|
||||
# Schema: key -> (expiry, encoding, value)
|
||||
self._conn.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS cache (
|
||||
ns TEXT,
|
||||
key TEXT,
|
||||
expiry REAL,
|
||||
encoding TEXT NOT NULL,
|
||||
val BLOB NOT NULL,
|
||||
PRIMARY KEY (ns, key)
|
||||
)"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Get the cached values for the given keys."""
|
||||
with self._lock, self._conn:
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
if not keys:
|
||||
return {}
|
||||
placeholders = ",".join("(?, ?)" for _ in keys)
|
||||
params: list[str] = []
|
||||
for ns_tuple, key in keys:
|
||||
params.extend((",".join(ns_tuple), key))
|
||||
cursor = self._conn.execute(
|
||||
f"SELECT ns, key, expiry, encoding, val FROM cache WHERE (ns, key) IN ({placeholders})",
|
||||
tuple(params),
|
||||
)
|
||||
values: dict[FullKey, ValueT] = {}
|
||||
rows = cursor.fetchall()
|
||||
for ns, key, expiry, encoding, raw in rows:
|
||||
if expiry is not None and now > expiry:
|
||||
# purge expired entry
|
||||
self._conn.execute(
|
||||
"DELETE FROM cache WHERE (ns, key) = (?, ?)", (ns, key)
|
||||
)
|
||||
continue
|
||||
values[(tuple(ns.split(",")), key)] = self.serde.loads_typed(
|
||||
(encoding, raw)
|
||||
)
|
||||
return values
|
||||
|
||||
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Asynchronously get the cached values for the given keys."""
|
||||
return await asyncio.to_thread(self.get, keys)
|
||||
|
||||
def set(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Set the cached values for the given keys and TTLs."""
|
||||
with self._lock, self._conn:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
for key, (value, ttl) in mapping.items():
|
||||
if ttl is not None:
|
||||
delta = datetime.timedelta(seconds=ttl)
|
||||
expiry: float | None = (now + delta).timestamp()
|
||||
else:
|
||||
expiry = None
|
||||
encoding, raw = self.serde.dumps_typed(value)
|
||||
self._conn.execute(
|
||||
"INSERT OR REPLACE INTO cache (ns, key, expiry, encoding, val) VALUES (?, ?, ?, ?, ?)",
|
||||
(",".join(key[0]), key[1], expiry, encoding, raw),
|
||||
)
|
||||
|
||||
async def aset(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Asynchronously set the cached values for the given keys and TTLs."""
|
||||
await asyncio.to_thread(self.set, mapping)
|
||||
|
||||
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
with self._lock, self._conn:
|
||||
if namespaces is None:
|
||||
self._conn.execute("DELETE FROM cache")
|
||||
else:
|
||||
placeholders = ",".join("?" for _ in namespaces)
|
||||
self._conn.execute(
|
||||
f"DELETE FROM cache WHERE (ns) IN ({placeholders})",
|
||||
tuple(",".join(key) for key in namespaces),
|
||||
)
|
||||
|
||||
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Asynchronously delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
await asyncio.to_thread(self.clear, namespaces)
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self._conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from typing import Generic, Sequence, TypeVar
|
||||
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
ValueT = TypeVar("ValueT")
|
||||
Namespace = tuple[str, ...]
|
||||
FullKey = tuple[Namespace, str]
|
||||
|
||||
|
||||
class BaseCache(ABC, Generic[ValueT]):
|
||||
"""Base class for a cache."""
|
||||
|
||||
serde: SerializerProtocol = JsonPlusSerializer(pickle_fallback=True)
|
||||
|
||||
def __init__(self, *, serde: SerializerProtocol | None = None) -> None:
|
||||
"""Initialize the cache with a serializer."""
|
||||
self.serde = serde or self.serde
|
||||
|
||||
@abstractmethod
|
||||
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Get the cached values for the given keys."""
|
||||
|
||||
@abstractmethod
|
||||
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Asynchronously get the cached values for the given keys."""
|
||||
|
||||
@abstractmethod
|
||||
def set(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Set the cached values for the given keys and TTLs."""
|
||||
|
||||
@abstractmethod
|
||||
async def aset(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Asynchronously set the cached values for the given keys and TTLs."""
|
||||
|
||||
@abstractmethod
|
||||
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
|
||||
@abstractmethod
|
||||
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Asynchronously delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
Vendored
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import threading
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
|
||||
class InMemoryCache(BaseCache[ValueT]):
|
||||
def __init__(self, *, serde: SerializerProtocol | None = None):
|
||||
super().__init__(serde=serde)
|
||||
self._cache: dict[Namespace, dict[str, tuple[str, bytes, float | None]]] = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Get the cached values for the given keys."""
|
||||
with self._lock:
|
||||
if not keys:
|
||||
return {}
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
values: dict[FullKey, ValueT] = {}
|
||||
for ns_tuple, key in keys:
|
||||
ns = Namespace(ns_tuple)
|
||||
if ns in self._cache and key in self._cache[ns]:
|
||||
enc, val, expiry = self._cache[ns][key]
|
||||
if expiry is None or now < expiry:
|
||||
values[(ns, key)] = self.serde.loads_typed((enc, val))
|
||||
else:
|
||||
del self._cache[ns][key]
|
||||
return values
|
||||
|
||||
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Asynchronously get the cached values for the given keys."""
|
||||
return self.get(keys)
|
||||
|
||||
def set(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Set the cached values for the given keys."""
|
||||
with self._lock:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
for (ns, key), (value, ttl) in keys.items():
|
||||
if ttl is not None:
|
||||
delta = datetime.timedelta(seconds=ttl)
|
||||
expiry: float | None = (now + delta).timestamp()
|
||||
else:
|
||||
expiry = None
|
||||
if ns not in self._cache:
|
||||
self._cache[ns] = {}
|
||||
self._cache[ns][key] = (
|
||||
*self.serde.dumps_typed(value),
|
||||
expiry,
|
||||
)
|
||||
|
||||
async def aset(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Asynchronously set the cached values for the given keys."""
|
||||
self.set(keys)
|
||||
|
||||
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
with self._lock:
|
||||
if namespaces is None:
|
||||
self._cache.clear()
|
||||
else:
|
||||
for ns in namespaces:
|
||||
if ns in self._cache:
|
||||
del self._cache[ns]
|
||||
|
||||
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Asynchronously delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
self.clear(namespaces)
|
||||
@@ -68,8 +68,9 @@ class InMemorySaver(
|
||||
str, dict[str, tuple[tuple[str, bytes], tuple[str, bytes], Optional[str]]]
|
||||
],
|
||||
]
|
||||
# (thread ID, checkpoint NS, checkpoint ID) -> (task ID, write idx)
|
||||
writes: defaultdict[
|
||||
tuple[str, str, str], # thread ID, checkpoint NS, checkpoint ID
|
||||
tuple[str, str, str],
|
||||
dict[tuple[str, int], tuple[str, str, tuple[str, bytes], str]],
|
||||
]
|
||||
blobs: dict[
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class SerializerProtocol(Protocol):
|
||||
class UntypedSerializerProtocol(Protocol):
|
||||
"""Protocol for serialization and deserialization of objects."""
|
||||
|
||||
def dumps(self, obj: Any) -> bytes: ...
|
||||
|
||||
def loads(self, data: bytes) -> Any: ...
|
||||
|
||||
|
||||
class SerializerProtocol(UntypedSerializerProtocol, Protocol):
|
||||
"""Protocol for serialization and deserialization of objects.
|
||||
|
||||
- `dumps`: Serialize an object to bytes.
|
||||
@@ -12,17 +22,13 @@ class SerializerProtocol(Protocol):
|
||||
Valid implementations include the `pickle`, `json` and `orjson` modules.
|
||||
"""
|
||||
|
||||
def dumps(self, obj: Any) -> bytes: ...
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ...
|
||||
|
||||
def loads(self, data: bytes) -> Any: ...
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any: ...
|
||||
|
||||
|
||||
class SerializerCompat(SerializerProtocol):
|
||||
def __init__(self, serde: SerializerProtocol) -> None:
|
||||
def __init__(self, serde: UntypedSerializerProtocol) -> None:
|
||||
self.serde = serde
|
||||
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
@@ -38,7 +44,9 @@ class SerializerCompat(SerializerProtocol):
|
||||
return self.serde.loads(data[1])
|
||||
|
||||
|
||||
def maybe_add_typed_methods(serde: SerializerProtocol) -> SerializerProtocol:
|
||||
def maybe_add_typed_methods(
|
||||
serde: SerializerProtocol | UntypedSerializerProtocol,
|
||||
) -> SerializerProtocol:
|
||||
"""Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
|
||||
|
||||
if not hasattr(serde, "loads_typed") or not hasattr(serde, "dumps_typed"):
|
||||
|
||||
@@ -3,6 +3,7 @@ import decimal
|
||||
import importlib
|
||||
import json
|
||||
import pathlib
|
||||
import pickle
|
||||
import re
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
@@ -37,8 +38,12 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
"""Serializer that uses ormsgpack, with a fallback to extended JSON serializer."""
|
||||
|
||||
def __init__(
|
||||
self, *, __unpack_ext_hook__: Optional[Callable[[int, bytes], Any]] = None
|
||||
self,
|
||||
*,
|
||||
pickle_fallback: bool = False,
|
||||
__unpack_ext_hook__: Optional[Callable[[int, bytes], Any]] = None,
|
||||
) -> None:
|
||||
self.pickle_fallback = pickle_fallback
|
||||
self._unpack_ext_hook = (
|
||||
__unpack_ext_hook__
|
||||
if __unpack_ext_hook__ is not None
|
||||
@@ -209,6 +214,8 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
except ormsgpack.MsgpackEncodeError as exc:
|
||||
if "valid UTF-8" in str(exc):
|
||||
return "json", self.dumps(obj)
|
||||
elif self.pickle_fallback:
|
||||
return "pickle", pickle.dumps(obj)
|
||||
raise exc
|
||||
|
||||
def loads(self, data: bytes) -> Any:
|
||||
@@ -228,6 +235,8 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return ormsgpack.unpackb(
|
||||
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
elif self.pickle_fallback and type_ == "pickle":
|
||||
return pickle.loads(data_)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown serialization type: {type_}")
|
||||
|
||||
|
||||
@@ -175,6 +175,13 @@ def cli():
|
||||
help="Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly."
|
||||
" Useful if you want to test against an image already built using `langgraph build`.",
|
||||
)
|
||||
@click.option(
|
||||
"--base-image",
|
||||
default=None,
|
||||
help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
|
||||
"\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
|
||||
"\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
|
||||
)
|
||||
@click.option(
|
||||
"--wait",
|
||||
is_flag=True,
|
||||
@@ -195,6 +202,7 @@ def up(
|
||||
debugger_base_url: Optional[str],
|
||||
postgres_uri: Optional[str],
|
||||
image: Optional[str],
|
||||
base_image: Optional[str],
|
||||
):
|
||||
click.secho("Starting LangGraph API server...", fg="green")
|
||||
click.secho(
|
||||
@@ -216,6 +224,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
)
|
||||
# add up + options
|
||||
args.extend(["up", "--remove-orphans"])
|
||||
@@ -342,7 +351,9 @@ def _build(
|
||||
)
|
||||
@click.option(
|
||||
"--base-image",
|
||||
hidden=True,
|
||||
help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
|
||||
"\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
|
||||
"\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
|
||||
)
|
||||
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@cli.command(
|
||||
@@ -440,7 +451,9 @@ tests
|
||||
)
|
||||
@click.option(
|
||||
"--base-image",
|
||||
help="Base image to use for the LangGraph API server. Defaults to langchain/langgraph-api or langchain/langgraphjs-api",
|
||||
help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
|
||||
"\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
|
||||
"\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
|
||||
)
|
||||
@log_command
|
||||
def dockerfile(
|
||||
@@ -489,6 +502,7 @@ def dockerfile(
|
||||
compose_dict = langgraph_cli.docker.compose_as_dict(
|
||||
capabilities,
|
||||
port=8123,
|
||||
base_image=base_image,
|
||||
)
|
||||
# Add .env file to the docker-compose.yml for the langgraph-api service
|
||||
compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
|
||||
@@ -497,6 +511,11 @@ def dockerfile(
|
||||
"context": ".",
|
||||
"dockerfile": save_path.name,
|
||||
}
|
||||
# Add the base_image as build arg if provided
|
||||
if base_image:
|
||||
compose_dict["services"]["langgraph-api"]["build"]["args"] = {
|
||||
"BASE_IMAGE": base_image
|
||||
}
|
||||
f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
|
||||
secho("✅ Created: docker-compose.yml", fg="green")
|
||||
|
||||
@@ -732,6 +751,7 @@ def prepare_args_and_stdin(
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image, # Pass image to compose YAML generator
|
||||
base_image=base_image,
|
||||
)
|
||||
args = [
|
||||
"--project-directory",
|
||||
@@ -766,6 +786,7 @@ def prepare(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
base_image: Optional[str] = None,
|
||||
) -> Tuple[List[str], str]:
|
||||
"""Prepare the arguments and stdin for running the LangGraph API server."""
|
||||
config_json = langgraph_cli.config.validate_config_file(config_path)
|
||||
@@ -775,7 +796,7 @@ def prepare(
|
||||
subp_exec(
|
||||
"docker",
|
||||
"pull",
|
||||
langgraph_cli.config.docker_tag(config_json),
|
||||
langgraph_cli.config.docker_tag(config_json, base_image),
|
||||
verbose=verbose,
|
||||
)
|
||||
)
|
||||
@@ -791,5 +812,6 @@ def prepare(
|
||||
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
)
|
||||
return args, stdin
|
||||
|
||||
@@ -145,6 +145,8 @@ def compose_as_dict(
|
||||
postgres_uri: Optional[str] = None,
|
||||
# If you are running against an already-built image, you can pass it here
|
||||
image: Optional[str] = None,
|
||||
# Base image to use for the LangGraph API server
|
||||
base_image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a docker compose file as a dictionary in YML style."""
|
||||
if postgres_uri is None:
|
||||
@@ -249,6 +251,7 @@ def compose(
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
base_image: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Create a docker compose file as a string."""
|
||||
compose_content = compose_as_dict(
|
||||
@@ -258,6 +261,7 @@ def compose(
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
)
|
||||
compose_str = dict_to_yaml(compose_content)
|
||||
return compose_str
|
||||
|
||||
Generated
+165
-149
@@ -148,105 +148,105 @@ pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
version = "3.4.2"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
|
||||
{file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
|
||||
{file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
|
||||
{file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
|
||||
{file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
|
||||
{file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
|
||||
{file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
|
||||
{file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
|
||||
{file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"},
|
||||
{file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"},
|
||||
{file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -310,48 +310,50 @@ markers = {main = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.2"
|
||||
version = "44.0.3"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = true
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"},
|
||||
{file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
|
||||
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -364,7 +366,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@@ -585,15 +587,15 @@ tests = ["flask (>=2.2.5)", "hypothesis (>=6.79.4)", "pytest (>=7.4.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.56"
|
||||
version = "0.3.59"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.56-py3-none-any.whl", hash = "sha256:a20c6aca0fa0da265d96d3b14a5a01828ac5d2d9d27516434873d76f2d4839ed"},
|
||||
{file = "langchain_core-0.3.56.tar.gz", hash = "sha256:de896585bc56e12652327dcd195227c3739a07e86e587c91a07101e0df11dffe"},
|
||||
{file = "langchain_core-0.3.59-py3-none-any.whl", hash = "sha256:9686baaff43f2c8175535da13faf40e6866769015e93130c3c1e4243e7244d70"},
|
||||
{file = "langchain_core-0.3.59.tar.gz", hash = "sha256:052a37cf298c505144f007e5aeede6ecff2dc92c827525d1ef59101eb3a4551c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -610,15 +612,15 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph-0.4.1-py3-none-any.whl", hash = "sha256:ad0a5fb4707ec46eb69a9905d629e3712ac14d58bd41fc63df18502dbb8e44b9"},
|
||||
{file = "langgraph-0.4.1.tar.gz", hash = "sha256:c6de009e638c3128232e8defa6e9a3218c03bcc2348ec7f06fba23ffcef4b98d"},
|
||||
{file = "langgraph-0.4.3-py3-none-any.whl", hash = "sha256:dec926e034f4d440b92a3c52139cb6e9763bc1791e79a6ea53a233309cec864f"},
|
||||
{file = "langgraph-0.4.3.tar.gz", hash = "sha256:272d5d5903f2c2882dbeeba849846a0f2500bd83fb3734a3801ebe64c1a60bdd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -631,15 +633,15 @@ xxhash = ">=3.5.0,<4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.1.23"
|
||||
version = "0.2.19"
|
||||
description = ""
|
||||
optional = true
|
||||
python-versions = ">=3.11"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_api-0.1.23-py3-none-any.whl", hash = "sha256:61a4ce49f12348118a19005412761d57940b10364d55d79562eac79eec5cea79"},
|
||||
{file = "langgraph_api-0.1.23.tar.gz", hash = "sha256:e978b3c8ef0f0f4808c35525dac4a55ff2de898ad533457a872b5464eb7e6fd6"},
|
||||
{file = "langgraph_api-0.2.19-py3-none-any.whl", hash = "sha256:40be8c3ca1238f67e564f8f56e2c6f0afe5c8ee517a4077eebb787d95cb70f9f"},
|
||||
{file = "langgraph_api-0.2.19.tar.gz", hash = "sha256:5ceabd04082ff6e0c8f4cdb8c7eed44ec8c22381728526619315fe4011266188"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -648,10 +650,10 @@ cryptography = ">=42.0.0,<45.0"
|
||||
httpx = ">=0.25.0"
|
||||
jsonschema-rs = ">=0.20.0,<0.30"
|
||||
langchain-core = {version = ">=0.2.38", markers = "python_version < \"4.0\""}
|
||||
langgraph = {version = ">=0.2.56", markers = "python_version < \"4.0\""}
|
||||
langgraph = {version = ">=0.3.27", markers = "python_version < \"4.0\""}
|
||||
langgraph-checkpoint = {version = ">=2.0.23", markers = "python_version < \"4.0\""}
|
||||
langgraph-runtime-inmem = ">=0.0.7"
|
||||
langgraph-sdk = {version = ">=0.1.63,<0.2.0", markers = "python_version < \"4.0\""}
|
||||
langgraph-runtime-inmem = ">=0.0.9,<0.1"
|
||||
langgraph-sdk = {version = ">=0.1.66,<0.2.0", markers = "python_version < \"4.0\""}
|
||||
langsmith = ">=0.1.63"
|
||||
orjson = ">=3.9.7"
|
||||
pyjwt = ">=2.9.0,<3.0.0"
|
||||
@@ -659,6 +661,7 @@ sse-starlette = ">=2.1.0,<2.2.0"
|
||||
starlette = ">=0.38.6"
|
||||
structlog = ">=24.1.0,<26"
|
||||
tenacity = ">=8.0.0"
|
||||
truststore = ">=0.1"
|
||||
uvicorn = ">=0.26.0"
|
||||
watchfiles = ">=0.13"
|
||||
|
||||
@@ -698,15 +701,15 @@ langgraph-checkpoint = ">=2.0.10,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-runtime-inmem"
|
||||
version = "0.0.8"
|
||||
version = "0.0.10"
|
||||
description = "Inmem implementation for the LangGraph API server."
|
||||
optional = true
|
||||
python-versions = ">=3.11.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_runtime_inmem-0.0.8-py3-none-any.whl", hash = "sha256:d0549225281ed4426f1cc9fe2610b0e0bf922775086cd822e951982bcfd64b68"},
|
||||
{file = "langgraph_runtime_inmem-0.0.8.tar.gz", hash = "sha256:3010a8dc49c245a2663b32879e648407f7ed78d6cf9671a5ced4fce9c7a6f738"},
|
||||
{file = "langgraph_runtime_inmem-0.0.10-py3-none-any.whl", hash = "sha256:346f570d09afd181e43bf9e09ce49c4406ff48c704a8de66ffd31b5b74952edb"},
|
||||
{file = "langgraph_runtime_inmem-0.0.10.tar.gz", hash = "sha256:3e5afbce3d6de276d918a556f20597005b988e12e65b574cfc194f4edad6d171"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -736,15 +739,15 @@ orjson = ">=3.10.1"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.3.39"
|
||||
version = "0.3.42"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langsmith-0.3.39-py3-none-any.whl", hash = "sha256:0c2af42e943e112bd0a1bd7452922141945b71f08df32cc5d6faa637100bc5e3"},
|
||||
{file = "langsmith-0.3.39.tar.gz", hash = "sha256:1624a70efe1c9378ed0802618e9f7cc14e45d50ec4a9b3af45137fa27ad95690"},
|
||||
{file = "langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff"},
|
||||
{file = "langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1572,6 +1575,19 @@ files = [
|
||||
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.1"
|
||||
description = "Verify certificates using native system trust stores"
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "truststore-0.10.1-py3-none-any.whl", hash = "sha256:b64e6025a409a43ebdd2807b0c41c8bff49ea7ae6550b5087ac6df6619352d4c"},
|
||||
{file = "truststore-0.10.1.tar.gz", hash = "sha256:eda021616b59021812e800fa0a071e51b266721bef3ce092db8a699e21c63539"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.langgraph_api/
|
||||
.devserver.pid
|
||||
+30
-4
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint test test_watch integration_tests spell_check spell_fix benchmark profile
|
||||
.PHONY: all format lint test test_watch integration_tests spell_check spell_fix benchmark profile start-dev-server integration_tests
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -40,20 +40,42 @@ start-postgres:
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
start-dev-server:
|
||||
poetry run langgraph dev --config tests/example_app/langgraph.json --no-browser &
|
||||
@echo "Dev server started."
|
||||
@echo "Dev server PID: $$!" > .devserver.pid
|
||||
|
||||
stop-dev-server:
|
||||
@if [ -f .devserver.pid ]; then \
|
||||
kill `cat .devserver.pid` && rm .devserver.pid; \
|
||||
echo "Dev server stopped."; \
|
||||
else \
|
||||
echo "No dev server PID file found."; \
|
||||
fi
|
||||
|
||||
TEST ?= .
|
||||
|
||||
test:
|
||||
make start-postgres && poetry run pytest $(TEST); \
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
poetry run pytest $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_parallel:
|
||||
make start-postgres && poetry run pytest -n auto --dist worksteal $(TEST); \
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
poetry run pytest -n auto --dist worksteal $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
integration_tests:
|
||||
poetry run pytest integration_tests
|
||||
|
||||
WORKERS ?= auto
|
||||
XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
|
||||
MAXFAIL ?=
|
||||
@@ -62,14 +84,18 @@ MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
|
||||
XDIST_ARGS := $(if $(WORKERS),-x $(XDIST_ARGS),)
|
||||
|
||||
test_watch:
|
||||
make start-postgres && poetry run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
poetry run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_watch_all:
|
||||
npx concurrently -n langgraph,checkpoint,checkpoint-sqlite,postgres "make test_watch" "make -C ../checkpoint test_watch" "make -C ../checkpoint-sqlite test_watch" "make -C ../checkpoint-postgres test_watch"
|
||||
|
||||
|
||||
######################
|
||||
# LINTING AND FORMATTING
|
||||
######################
|
||||
|
||||
@@ -53,16 +53,7 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
"""Return a new identical channel, optionally initialized from a checkpoint.
|
||||
If the checkpoint contains complex data structures, they should be copied."""
|
||||
|
||||
# state methods
|
||||
|
||||
@abstractmethod
|
||||
def update(self, values: Sequence[Update]) -> bool:
|
||||
"""Update the channel's value with the given sequence of updates.
|
||||
The order of the updates in the sequence is arbitrary.
|
||||
This method is called by Pregel for all channels at the end of each step.
|
||||
If there are no updates, it is called with an empty sequence.
|
||||
Raises InvalidUpdateError if the sequence of updates is invalid.
|
||||
Returns True if the channel was updated, False otherwise."""
|
||||
# read methods
|
||||
|
||||
@abstractmethod
|
||||
def get(self) -> Value:
|
||||
@@ -70,13 +61,6 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
|
||||
Raises EmptyChannelError if the channel is empty (never updated yet)."""
|
||||
|
||||
def consume(self) -> bool:
|
||||
"""Mark the current value of the channel as consumed. By default, no-op.
|
||||
This is called by Pregel before the start of the next step, for all
|
||||
channels that triggered a node. If the channel was updated, return True.
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Return True if the channel is available (not empty), False otherwise.
|
||||
Subclasses should override this method to provide a more efficient
|
||||
@@ -88,6 +72,34 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
except EmptyChannelError:
|
||||
return False
|
||||
|
||||
# write methods
|
||||
|
||||
@abstractmethod
|
||||
def update(self, values: Sequence[Update]) -> bool:
|
||||
"""Update the channel's value with the given sequence of updates.
|
||||
The order of the updates in the sequence is arbitrary.
|
||||
This method is called by Pregel for all channels at the end of each step.
|
||||
If there are no updates, it is called with an empty sequence.
|
||||
Raises InvalidUpdateError if the sequence of updates is invalid.
|
||||
Returns True if the channel was updated, False otherwise."""
|
||||
|
||||
def consume(self) -> bool:
|
||||
"""Notify the channel that a subscribed task ran. By default, no-op.
|
||||
A channel can use this method to modify its state, preventing the value
|
||||
from being consumed again.
|
||||
|
||||
Returns True if the channel was updated, False otherwise.
|
||||
"""
|
||||
return False
|
||||
|
||||
def finish(self) -> bool:
|
||||
"""Notify the channel that the Pregel run is finishing. By default, no-op.
|
||||
A channel can use this method to modify its state, preventing finish.
|
||||
|
||||
Returns True if the channel was updated, False otherwise.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseChannel",
|
||||
|
||||
@@ -81,12 +81,9 @@ class DynamicBarrierValue(
|
||||
updated = False
|
||||
for value in values:
|
||||
assert not isinstance(value, WaitForNames)
|
||||
if value in self.names:
|
||||
if value not in self.seen:
|
||||
self.seen.add(value)
|
||||
updated = True
|
||||
else:
|
||||
raise InvalidUpdateError(f"Value {value} not in {self.names}")
|
||||
if value in self.names and value not in self.seen:
|
||||
self.seen.add(value)
|
||||
updated = True
|
||||
return updated
|
||||
|
||||
def get(self) -> Value:
|
||||
@@ -103,3 +100,107 @@ class DynamicBarrierValue(
|
||||
self.names = None
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DynamicBarrierValueAfterFinish(
|
||||
Generic[Value], BaseChannel[Value, Union[Value, WaitForNames], Set[Value]]
|
||||
):
|
||||
"""A channel that switches between two states
|
||||
|
||||
- in the "priming" state it can't be read from.
|
||||
- if it receives a WaitForNames update, it switches to the "waiting" state.
|
||||
- in the "waiting" state it collects named values until all are received.
|
||||
- once all named values are received, and the finished flag is set, it can be read once, and it switches
|
||||
back to the "priming" state.
|
||||
"""
|
||||
|
||||
__slots__ = ("names", "seen", "finished")
|
||||
|
||||
names: Optional[Set[Value]]
|
||||
seen: set[Value]
|
||||
finished: bool
|
||||
|
||||
def __init__(self, typ: type[Value]) -> None:
|
||||
super().__init__(typ)
|
||||
self.names = None
|
||||
self.seen = set()
|
||||
self.finished = False
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, DynamicBarrierValueAfterFinish)
|
||||
and value.names == self.names
|
||||
)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
empty.names = self.names
|
||||
empty.seen = self.seen.copy()
|
||||
empty.finished = self.finished
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> tuple[Optional[Set[Value]], set[Value], bool]:
|
||||
return (self.names, self.seen, self.finished)
|
||||
|
||||
def from_checkpoint(
|
||||
self, checkpoint: tuple[Optional[Set[Value]], set[Value], bool]
|
||||
) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
names, seen, finished = checkpoint
|
||||
empty.names = names if names is not None else None
|
||||
empty.seen = seen
|
||||
empty.finished = finished
|
||||
return empty
|
||||
|
||||
def update(self, values: Sequence[Union[Value, WaitForNames]]) -> bool:
|
||||
if wait_for_names := [v for v in values if isinstance(v, WaitForNames)]:
|
||||
if len(wait_for_names) > 1:
|
||||
raise InvalidUpdateError(
|
||||
f"At key '{self.key}': Received multiple WaitForNames updates in the same step."
|
||||
)
|
||||
self.names = wait_for_names[0].names
|
||||
return True
|
||||
elif self.names is not None:
|
||||
updated = False
|
||||
for value in values:
|
||||
assert not isinstance(value, WaitForNames)
|
||||
if value in self.names and value not in self.seen:
|
||||
self.seen.add(value)
|
||||
updated = True
|
||||
return updated
|
||||
|
||||
def get(self) -> Value:
|
||||
if not self.finished and self.seen != self.names:
|
||||
raise EmptyChannelError()
|
||||
return None
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.seen == self.names and self.finished
|
||||
|
||||
def consume(self) -> bool:
|
||||
if self.finished and self.seen == self.names:
|
||||
self.seen = set()
|
||||
self.names = None
|
||||
return True
|
||||
return False
|
||||
|
||||
def finish(self) -> bool:
|
||||
if not self.finished and self.seen == self.names:
|
||||
self.finished = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -70,3 +70,73 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
|
||||
class LastValueAfterFinish(
|
||||
Generic[Value], BaseChannel[Value, Value, tuple[Value, bool]]
|
||||
):
|
||||
"""Stores the last value received, but only made available after finish().
|
||||
Once made available, clears the value."""
|
||||
|
||||
__slots__ = ("value", "finished")
|
||||
|
||||
def __init__(self, typ: Any, key: str = "") -> None:
|
||||
super().__init__(typ, key)
|
||||
self.value = MISSING
|
||||
self.finished = False
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, LastValueAfterFinish)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def checkpoint(self) -> tuple[Value, bool]:
|
||||
if self.value is MISSING:
|
||||
return MISSING
|
||||
return (self.value, self.finished)
|
||||
|
||||
def from_checkpoint(self, checkpoint: tuple[Value, bool]) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
empty.value, empty.finished = checkpoint
|
||||
return empty
|
||||
|
||||
def update(self, values: Sequence[Value]) -> bool:
|
||||
if len(values) == 0:
|
||||
return False
|
||||
|
||||
self.finished = False
|
||||
self.value = values[-1]
|
||||
return True
|
||||
|
||||
def consume(self) -> bool:
|
||||
if self.finished:
|
||||
self.finished = False
|
||||
self.value = MISSING
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def finish(self) -> bool:
|
||||
if not self.finished and self.value is not MISSING:
|
||||
self.finished = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get(self) -> Value:
|
||||
if self.value is MISSING or not self.finished:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING and self.finished
|
||||
|
||||
@@ -77,3 +77,89 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
self.seen = set()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class NamedBarrierValueAfterFinish(
|
||||
Generic[Value], BaseChannel[Value, Value, set[Value]]
|
||||
):
|
||||
"""A channel that waits until all named values are received before making the value ready to be made available. It is only made available after finish() is called."""
|
||||
|
||||
__slots__ = ("names", "seen", "finished")
|
||||
|
||||
names: set[Value]
|
||||
seen: set[Value]
|
||||
|
||||
def __init__(self, typ: type[Value], names: set[Value]) -> None:
|
||||
super().__init__(typ)
|
||||
self.names = names
|
||||
self.seen: set[str] = set()
|
||||
self.finished = False
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, NamedBarrierValueAfterFinish)
|
||||
and value.names == self.names
|
||||
)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
empty.seen = self.seen.copy()
|
||||
empty.finished = self.finished
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> tuple[set[Value], bool]:
|
||||
return (self.seen, self.finished)
|
||||
|
||||
def from_checkpoint(self, checkpoint: tuple[set[Value], bool]) -> Self:
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
empty.seen, empty.finished = checkpoint
|
||||
return empty
|
||||
|
||||
def update(self, values: Sequence[Value]) -> bool:
|
||||
updated = False
|
||||
for value in values:
|
||||
if value in self.names:
|
||||
if value not in self.seen:
|
||||
self.seen.add(value)
|
||||
updated = True
|
||||
else:
|
||||
raise InvalidUpdateError(
|
||||
f"At key '{self.key}': Value {value} not in {self.names}"
|
||||
)
|
||||
return updated
|
||||
|
||||
def get(self) -> Value:
|
||||
if not self.finished or self.seen != self.names:
|
||||
raise EmptyChannelError()
|
||||
return None
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.finished and self.seen == self.names
|
||||
|
||||
def consume(self) -> bool:
|
||||
if self.finished and self.seen == self.names:
|
||||
self.finished = False
|
||||
self.seen = set()
|
||||
return True
|
||||
return False
|
||||
|
||||
def finish(self) -> bool:
|
||||
if not self.finished and self.seen == self.names:
|
||||
self.finished = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -46,6 +46,10 @@ TASKS = sys.intern("__pregel_tasks")
|
||||
RETURN = sys.intern("__return__")
|
||||
# for writes of a task where we simply record the return value
|
||||
|
||||
# --- Reserved cache namespaces ---
|
||||
CACHE_NS_WRITES = sys.intern("__pregel_ns_writes")
|
||||
# cache namespace for node writes
|
||||
|
||||
# --- Reserved config.configurable keys ---
|
||||
CONFIG_KEY_SEND = sys.intern("__pregel_send")
|
||||
# holds the `write` function that accepts writes to state/edges/reserved keys
|
||||
@@ -61,6 +65,8 @@ CONFIG_KEY_STREAM_WRITER = sys.intern("__pregel_stream_writer")
|
||||
# holds a `StreamWriter` for stream_mode=custom
|
||||
CONFIG_KEY_STORE = sys.intern("__pregel_store")
|
||||
# holds a `BaseStore` made available to managed values
|
||||
CONFIG_KEY_CACHE = sys.intern("__pregel_cache")
|
||||
# holds a `BaseCache` made available to subgraphs
|
||||
CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming")
|
||||
# holds a boolean indicating if subgraphs should resume from a previous checkpoint
|
||||
CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id")
|
||||
|
||||
@@ -16,10 +16,11 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, PREVIOUS, START
|
||||
from langgraph.constants import CACHE_NS_WRITES, END, PREVIOUS, START
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.pregel.call import (
|
||||
P,
|
||||
@@ -27,11 +28,55 @@ from langgraph.pregel.call import (
|
||||
T,
|
||||
call,
|
||||
get_runnable_for_entrypoint,
|
||||
identifier,
|
||||
)
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import _DC_KWARGS, RetryPolicy, StreamMode
|
||||
from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode
|
||||
|
||||
|
||||
class TaskFunction(Generic[P, T]):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[P, T],
|
||||
*,
|
||||
retry: Optional[Sequence[RetryPolicy]] = (),
|
||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
if name is not None:
|
||||
if hasattr(func, "__func__"):
|
||||
# handle class methods
|
||||
# NOTE: we're modifying the instance method to avoid modifying
|
||||
# the original class method in case it's shared across multiple tasks
|
||||
instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [attr-defined]
|
||||
instance_method.__name__ = name # type: ignore [attr-defined]
|
||||
func = instance_method
|
||||
else:
|
||||
# handle regular functions / partials / callable classes, etc.
|
||||
func.__name__ = name
|
||||
self.func = func
|
||||
self.retry = retry
|
||||
self.cache_policy = cache_policy
|
||||
functools.update_wrapper(self, func)
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> SyncAsyncFuture[T]:
|
||||
return call(
|
||||
self.func, retry=self.retry, cache_policy=self.cache_policy, *args, **kwargs
|
||||
)
|
||||
|
||||
def clear_cache(self, cache: BaseCache) -> None:
|
||||
"""Clear the cache for this task."""
|
||||
if self.cache_policy is not None:
|
||||
cache.clear(((CACHE_NS_WRITES, identifier(self.func) or "__dynamic__"),))
|
||||
|
||||
async def aclear_cache(self, cache: BaseCache) -> None:
|
||||
"""Clear the cache for this task."""
|
||||
if self.cache_policy is not None:
|
||||
await cache.aclear(
|
||||
((CACHE_NS_WRITES, identifier(self.func) or "__dynamic__"),)
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
@@ -39,16 +84,17 @@ def task(
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
||||
) -> Callable[
|
||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
TaskFunction[P, T],
|
||||
]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
__func_or_none__: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
||||
) -> Callable[P, SyncAsyncFuture[T]]: ...
|
||||
) -> TaskFunction[P, T]: ...
|
||||
|
||||
|
||||
def task(
|
||||
@@ -56,12 +102,13 @@ def task(
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
||||
) -> Union[
|
||||
Callable[
|
||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
TaskFunction[P, T],
|
||||
],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
TaskFunction[P, T],
|
||||
]:
|
||||
"""Define a LangGraph task using the `task` decorator.
|
||||
|
||||
@@ -129,21 +176,9 @@ def task(
|
||||
) -> Union[
|
||||
Callable[P, concurrent.futures.Future[T]], Callable[P, asyncio.Future[T]]
|
||||
]:
|
||||
if name is not None:
|
||||
if hasattr(func, "__func__"):
|
||||
# handle class methods
|
||||
# NOTE: we're modifying the instance method to avoid modifying
|
||||
# the original class method in case it's shared across multiple tasks
|
||||
instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [union-attr]
|
||||
instance_method.__name__ = name # type: ignore [attr-defined]
|
||||
func = instance_method
|
||||
else:
|
||||
# handle regular functions / partials / callable classes, etc.
|
||||
func.__name__ = name
|
||||
|
||||
call_func = functools.partial(call, func, retry=retry_policies)
|
||||
object.__setattr__(call_func, "_is_pregel_task", True)
|
||||
return functools.update_wrapper(call_func, func)
|
||||
return TaskFunction(
|
||||
func, retry=retry_policies, cache_policy=cache_policy, name=name
|
||||
)
|
||||
|
||||
if __func_or_none__ is not None:
|
||||
return decorator(__func_or_none__)
|
||||
@@ -316,11 +351,17 @@ class entrypoint:
|
||||
self,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
cache: Optional[BaseCache] = None,
|
||||
config_schema: Optional[type[Any]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
retry: Union[RetryPolicy, Sequence[RetryPolicy]] = (),
|
||||
) -> None:
|
||||
"""Initialize the entrypoint decorator."""
|
||||
self.checkpointer = checkpointer
|
||||
self.store = store
|
||||
self.cache = cache
|
||||
self.cache_policy = cache_policy
|
||||
self.retry = retry
|
||||
self.config_schema = config_schema
|
||||
|
||||
@dataclass(**_DC_KWARGS)
|
||||
@@ -450,5 +491,8 @@ class entrypoint:
|
||||
stream_eager=True,
|
||||
checkpointer=self.checkpointer,
|
||||
store=self.store,
|
||||
cache=self.cache,
|
||||
cache_policy=self.cache_policy,
|
||||
retry_policy=self.retry,
|
||||
config_type=self.config_schema,
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
from langchain_core.runnables import Runnable
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.constants import (
|
||||
EMPTY_SEQ,
|
||||
@@ -28,6 +29,7 @@ from langgraph.graph.branch import Branch
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, Checkpointer
|
||||
from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
|
||||
|
||||
@@ -316,6 +318,9 @@ class Graph:
|
||||
interrupt_after: Optional[Union[All, list[str]]] = None,
|
||||
debug: bool = False,
|
||||
name: Optional[str] = None,
|
||||
*,
|
||||
cache: Optional[BaseCache] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
) -> "CompiledGraph":
|
||||
"""Compiles the graph into a `CompiledGraph` object.
|
||||
|
||||
@@ -364,6 +369,8 @@ class Graph:
|
||||
auto_validate=False,
|
||||
debug=debug,
|
||||
name=name or "LangGraph",
|
||||
cache=cache,
|
||||
store=store,
|
||||
)
|
||||
|
||||
# attach nodes, edges, and branches
|
||||
|
||||
@@ -26,12 +26,20 @@ from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph._api.deprecation import LangGraphDeprecationWarning
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitForNames
|
||||
from langgraph.channels.dynamic_barrier_value import (
|
||||
DynamicBarrierValue,
|
||||
DynamicBarrierValueAfterFinish,
|
||||
WaitForNames,
|
||||
)
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.channels.last_value import LastValue, LastValueAfterFinish
|
||||
from langgraph.channels.named_barrier_value import (
|
||||
NamedBarrierValue,
|
||||
NamedBarrierValueAfterFinish,
|
||||
)
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import (
|
||||
EMPTY_SEQ,
|
||||
@@ -72,7 +80,7 @@ from langgraph.pregel.write import (
|
||||
ChannelWriteTupleEntry,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, Checkpointer, Command, RetryPolicy
|
||||
from langgraph.types import All, CachePolicy, Checkpointer, Command, RetryPolicy
|
||||
from langgraph.utils.fields import get_field_default, get_update_as_tuples
|
||||
from langgraph.utils.pydantic import create_model
|
||||
from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
|
||||
@@ -106,7 +114,9 @@ class StateNodeSpec(NamedTuple):
|
||||
metadata: Optional[dict[str, Any]]
|
||||
input: type[Any]
|
||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]]
|
||||
cache_policy: Optional[CachePolicy]
|
||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
||||
defer: bool = False
|
||||
|
||||
|
||||
class StateGraph(Graph):
|
||||
@@ -247,9 +257,11 @@ class StateGraph(Graph):
|
||||
self,
|
||||
node: RunnableLike,
|
||||
*,
|
||||
defer: bool = False,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph.
|
||||
@@ -263,9 +275,11 @@ class StateGraph(Graph):
|
||||
node: str,
|
||||
action: RunnableLike,
|
||||
*,
|
||||
defer: bool = False,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph."""
|
||||
@@ -276,9 +290,11 @@ class StateGraph(Graph):
|
||||
node: Union[str, RunnableLike],
|
||||
action: Optional[RunnableLike] = None,
|
||||
*,
|
||||
defer: bool = False,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph.
|
||||
@@ -292,6 +308,7 @@ class StateGraph(Graph):
|
||||
input: The input schema for the node. (default: the graph's input schema)
|
||||
retry: The policy for retrying the node. (default: None)
|
||||
If a sequence is provided, the first matching policy will be applied.
|
||||
cache_policy: The cache policy for the node. (default: None)
|
||||
destinations: Destinations that indicate where a node can route to.
|
||||
This is useful for edgeless graphs with nodes that return `Command` objects.
|
||||
If a dict is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
|
||||
@@ -420,7 +437,9 @@ class StateGraph(Graph):
|
||||
metadata,
|
||||
input=input or self.schema,
|
||||
retry_policy=retry,
|
||||
cache_policy=cache_policy,
|
||||
ends=ends,
|
||||
defer=defer,
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -559,6 +578,7 @@ class StateGraph(Graph):
|
||||
self,
|
||||
checkpointer: Checkpointer = None,
|
||||
*,
|
||||
cache: Optional[BaseCache] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
interrupt_before: Optional[Union[All, list[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, list[str]]] = None,
|
||||
@@ -643,6 +663,7 @@ class StateGraph(Graph):
|
||||
auto_validate=False,
|
||||
debug=debug,
|
||||
store=store,
|
||||
cache=cache,
|
||||
name=name or "LangGraph",
|
||||
)
|
||||
|
||||
@@ -784,7 +805,11 @@ class CompiledStateGraph(CompiledGraph):
|
||||
self.schema_to_mapper[input_schema] = mapper
|
||||
|
||||
branch_channel = CHANNEL_BRANCH_TO.format(key)
|
||||
self.channels[branch_channel] = EphemeralValue(Any, guard=False)
|
||||
self.channels[branch_channel] = (
|
||||
LastValueAfterFinish(Any)
|
||||
if node.defer
|
||||
else EphemeralValue(Any, guard=False)
|
||||
)
|
||||
self.nodes[key] = PregelNode(
|
||||
triggers=[branch_channel],
|
||||
# read state keys and managed values
|
||||
@@ -795,6 +820,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
writers=[ChannelWrite(write_entries)],
|
||||
metadata=node.metadata,
|
||||
retry_policy=node.retry_policy,
|
||||
cache_policy=node.cache_policy,
|
||||
bound=node.runnable,
|
||||
)
|
||||
else:
|
||||
@@ -812,7 +838,12 @@ class CompiledStateGraph(CompiledGraph):
|
||||
elif end != END:
|
||||
channel_name = f"join:{'+'.join(starts)}:{end}"
|
||||
# register channel
|
||||
self.channels[channel_name] = NamedBarrierValue(str, set(starts))
|
||||
if self.builder.nodes[end].defer:
|
||||
self.channels[channel_name] = NamedBarrierValueAfterFinish(
|
||||
str, set(starts)
|
||||
)
|
||||
else:
|
||||
self.channels[channel_name] = NamedBarrierValue(str, set(starts))
|
||||
# subscribe to channel
|
||||
self.nodes[end].triggers.append(channel_name)
|
||||
# publish to channel
|
||||
@@ -889,7 +920,10 @@ class CompiledStateGraph(CompiledGraph):
|
||||
else [node for node in self.builder.nodes if node != branch.then]
|
||||
)
|
||||
channel_name = f"branch:{start}:{name}::then"
|
||||
self.channels[channel_name] = DynamicBarrierValue(str)
|
||||
if self.builder.nodes[branch.then].defer:
|
||||
self.channels[channel_name] = DynamicBarrierValueAfterFinish(str)
|
||||
else:
|
||||
self.channels[channel_name] = DynamicBarrierValue(str)
|
||||
self.nodes[branch.then].triggers.append(channel_name)
|
||||
for end in ends:
|
||||
if end != END:
|
||||
|
||||
@@ -36,6 +36,7 @@ from langchain_core.runnables.utils import (
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import (
|
||||
BaseChannel,
|
||||
)
|
||||
@@ -46,7 +47,9 @@ from langgraph.checkpoint.base import (
|
||||
copy_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CACHE_NS_WRITES,
|
||||
CONF,
|
||||
CONFIG_KEY_CACHE,
|
||||
CONFIG_KEY_CHECKPOINT_DURING,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
@@ -85,6 +88,7 @@ from langgraph.pregel.algo import (
|
||||
local_write,
|
||||
prepare_next_tasks,
|
||||
)
|
||||
from langgraph.pregel.call import identifier
|
||||
from langgraph.pregel.checkpoint import create_checkpoint, empty_checkpoint
|
||||
from langgraph.pregel.debug import tasks_w_writes
|
||||
from langgraph.pregel.draw import draw_graph
|
||||
@@ -102,6 +106,7 @@ from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CachePolicy,
|
||||
Checkpointer,
|
||||
Interrupt,
|
||||
LoopProtocol,
|
||||
@@ -495,8 +500,15 @@ class Pregel(PregelProtocol):
|
||||
store: BaseStore | None = None
|
||||
"""Memory store to use for SharedValues. Defaults to None."""
|
||||
|
||||
retry_policy: Sequence[RetryPolicy] | None = None
|
||||
"""Retry policies to use when running tasks. Set to None to disable."""
|
||||
cache: BaseCache | None = None
|
||||
"""Cache to use for storing node results. Defaults to None."""
|
||||
|
||||
retry_policy: Sequence[RetryPolicy] = ()
|
||||
"""Retry policies to use when running tasks. Empty set disables retries."""
|
||||
|
||||
cache_policy: CachePolicy | None = None
|
||||
"""Cache policy to use for all nodes. Can be overridden by individual nodes.
|
||||
Defaults to None."""
|
||||
|
||||
config_type: type[Any] | None = None
|
||||
|
||||
@@ -506,7 +518,7 @@ class Pregel(PregelProtocol):
|
||||
|
||||
name: str = "LangGraph"
|
||||
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -525,7 +537,9 @@ class Pregel(PregelProtocol):
|
||||
debug: bool | None = None,
|
||||
checkpointer: BaseCheckpointSaver | None = None,
|
||||
store: BaseStore | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||
cache: BaseCache | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] = (),
|
||||
cache_policy: CachePolicy | None = None,
|
||||
config_type: type[Any] | None = None,
|
||||
input_model: type[BaseModel] | None = None,
|
||||
config: RunnableConfig | None = None,
|
||||
@@ -545,14 +559,15 @@ class Pregel(PregelProtocol):
|
||||
self.debug = debug if debug is not None else get_debug()
|
||||
self.checkpointer = checkpointer
|
||||
self.store = store
|
||||
if isinstance(retry_policy, RetryPolicy):
|
||||
self.retry_policy: Sequence[RetryPolicy] = (retry_policy,)
|
||||
else:
|
||||
self.retry_policy = retry_policy
|
||||
self.cache = cache
|
||||
self.retry_policy = (
|
||||
(retry_policy,) if isinstance(retry_policy, RetryPolicy) else retry_policy
|
||||
)
|
||||
self.cache_policy = cache_policy
|
||||
self.config_type = config_type
|
||||
self.input_model = input_model
|
||||
self.config = config
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.trigger_to_nodes = trigger_to_nodes or {}
|
||||
self.name = name
|
||||
if auto_validate:
|
||||
self.validate()
|
||||
@@ -949,6 +964,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
if apply_pending_writes and saved.pending_writes:
|
||||
for tid, k, v in saved.pending_writes:
|
||||
@@ -958,7 +974,9 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(saved.checkpoint, channels, tasks, None)
|
||||
apply_writes(
|
||||
saved.checkpoint, channels, tasks, None, self.trigger_to_nodes
|
||||
)
|
||||
tasks_with_writes = tasks_w_writes(
|
||||
next_tasks.values(),
|
||||
saved.pending_writes,
|
||||
@@ -1071,6 +1089,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
if apply_pending_writes and saved.pending_writes:
|
||||
for tid, k, v in saved.pending_writes:
|
||||
@@ -1080,7 +1099,9 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(saved.checkpoint, channels, tasks, None)
|
||||
apply_writes(
|
||||
saved.checkpoint, channels, tasks, None, self.trigger_to_nodes
|
||||
)
|
||||
|
||||
tasks_with_writes = tasks_w_writes(
|
||||
next_tasks.values(),
|
||||
@@ -1407,6 +1428,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# apply writes from tasks that already ran
|
||||
for tid, k, v in saved.pending_writes or []:
|
||||
@@ -1416,7 +1438,13 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
# clear all current tasks
|
||||
apply_writes(checkpoint, channels, next_tasks.values(), None)
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
next_tasks.values(),
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# save checkpoint
|
||||
next_config = checkpointer.put(
|
||||
checkpoint_config,
|
||||
@@ -1475,6 +1503,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, input_writes, [])],
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
|
||||
# apply input write to channels
|
||||
@@ -1575,6 +1604,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# apply writes
|
||||
for tid, k, v in saved.pending_writes:
|
||||
@@ -1584,12 +1614,16 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
apply_writes(
|
||||
checkpoint, channels, tasks, None, self.trigger_to_nodes
|
||||
)
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node = updates[0]
|
||||
# find last node that updated the state, if not provided
|
||||
if as_node is None and not any(
|
||||
if as_node is None and len(self.nodes) == 1:
|
||||
as_node = tuple(self.nodes)[0]
|
||||
elif as_node is None and not any(
|
||||
v
|
||||
for vv in checkpoint["versions_seen"].values()
|
||||
for v in vv.values()
|
||||
@@ -1672,7 +1706,11 @@ class Pregel(PregelProtocol):
|
||||
checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
|
||||
# apply to checkpoint and save
|
||||
mv_writes, _ = apply_writes(
|
||||
checkpoint, channels, run_tasks, checkpointer.get_next_version
|
||||
checkpoint,
|
||||
channels,
|
||||
run_tasks,
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
assert not mv_writes, "Can't write to SharedValues from update_state"
|
||||
checkpoint = create_checkpoint(checkpoint, channels, step + 1)
|
||||
@@ -1822,6 +1860,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# apply writes from tasks that already ran
|
||||
for tid, k, v in saved.pending_writes or []:
|
||||
@@ -1831,7 +1870,13 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
# clear all current tasks
|
||||
apply_writes(checkpoint, channels, next_tasks.values(), None)
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
next_tasks.values(),
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# save checkpoint
|
||||
next_config = await checkpointer.aput(
|
||||
checkpoint_config,
|
||||
@@ -1890,6 +1935,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, input_writes, [])],
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
|
||||
# apply input write to channels
|
||||
@@ -1990,6 +2036,7 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
@@ -1998,12 +2045,16 @@ class Pregel(PregelProtocol):
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
apply_writes(
|
||||
checkpoint, channels, tasks, None, self.trigger_to_nodes
|
||||
)
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node = updates[0]
|
||||
# find last node that updated the state, if not provided
|
||||
if as_node is None and not saved:
|
||||
if as_node is None and len(self.nodes) == 1:
|
||||
as_node = tuple(self.nodes)[0]
|
||||
elif as_node is None and not saved:
|
||||
if (
|
||||
isinstance(self.input_channels, str)
|
||||
and self.input_channels in self.nodes
|
||||
@@ -2084,7 +2135,11 @@ class Pregel(PregelProtocol):
|
||||
)
|
||||
# apply to checkpoint and save
|
||||
mv_writes, _ = apply_writes(
|
||||
checkpoint, channels, run_tasks, checkpointer.get_next_version
|
||||
checkpoint,
|
||||
channels,
|
||||
run_tasks,
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
assert not mv_writes, "Can't write to SharedValues from update_state"
|
||||
checkpoint = create_checkpoint(checkpoint, channels, step + 1)
|
||||
@@ -2157,6 +2212,7 @@ class Pregel(PregelProtocol):
|
||||
All | Sequence[str],
|
||||
BaseCheckpointSaver | None,
|
||||
BaseStore | None,
|
||||
BaseCache | None,
|
||||
]:
|
||||
if config["recursion_limit"] < 1:
|
||||
raise ValueError("recursion_limit must be at least 1")
|
||||
@@ -2189,6 +2245,10 @@ class Pregel(PregelProtocol):
|
||||
store: BaseStore | None = config[CONF][CONFIG_KEY_STORE]
|
||||
else:
|
||||
store = self.store
|
||||
if CONFIG_KEY_CACHE in config.get(CONF, {}):
|
||||
cache: BaseCache | None = config[CONF][CONFIG_KEY_CACHE]
|
||||
else:
|
||||
cache = self.cache
|
||||
return (
|
||||
debug,
|
||||
set(stream_mode),
|
||||
@@ -2197,6 +2257,7 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after,
|
||||
checkpointer,
|
||||
store,
|
||||
cache,
|
||||
)
|
||||
|
||||
def stream(
|
||||
@@ -2369,6 +2430,7 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after_,
|
||||
checkpointer,
|
||||
store,
|
||||
cache,
|
||||
) = self._defaults(
|
||||
config,
|
||||
stream_mode=stream_mode,
|
||||
@@ -2400,6 +2462,7 @@ class Pregel(PregelProtocol):
|
||||
stream=StreamProtocol(stream.put, stream_modes),
|
||||
config=config,
|
||||
store=store,
|
||||
cache=cache,
|
||||
checkpointer=checkpointer,
|
||||
nodes=self.nodes,
|
||||
specs=self.channels,
|
||||
@@ -2414,6 +2477,8 @@ class Pregel(PregelProtocol):
|
||||
else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True),
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
migrate_checkpoint=self._migrate_checkpoint,
|
||||
retry_policy=self.retry_policy,
|
||||
cache_policy=self.cache_policy,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
@@ -2458,11 +2523,13 @@ class Pregel(PregelProtocol):
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps.
|
||||
while loop.tick(input_keys=self.input_channels):
|
||||
for task in loop.match_cached_writes():
|
||||
loop.output_writes(task.id, task.writes, cached=True)
|
||||
for _ in runner.tick(
|
||||
loop.tasks.values(),
|
||||
[t for t in loop.tasks.values() if not t.writes],
|
||||
timeout=self.step_timeout,
|
||||
retry_policy=self.retry_policy,
|
||||
get_waiter=get_waiter,
|
||||
match_cached_writes=loop.match_cached_writes,
|
||||
):
|
||||
# emit output
|
||||
yield from output()
|
||||
@@ -2674,6 +2741,7 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after_,
|
||||
checkpointer,
|
||||
store,
|
||||
cache,
|
||||
) = self._defaults(
|
||||
config,
|
||||
stream_mode=stream_mode,
|
||||
@@ -2707,6 +2775,7 @@ class Pregel(PregelProtocol):
|
||||
stream=StreamProtocol(stream.put_nowait, stream_modes),
|
||||
config=config,
|
||||
store=store,
|
||||
cache=cache,
|
||||
checkpointer=checkpointer,
|
||||
nodes=self.nodes,
|
||||
specs=self.channels,
|
||||
@@ -2721,6 +2790,8 @@ class Pregel(PregelProtocol):
|
||||
else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True),
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
migrate_checkpoint=self._migrate_checkpoint,
|
||||
retry_policy=self.retry_policy,
|
||||
cache_policy=self.cache_policy,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
@@ -2756,11 +2827,13 @@ class Pregel(PregelProtocol):
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps
|
||||
while loop.tick(input_keys=self.input_channels):
|
||||
for task in await loop.amatch_cached_writes():
|
||||
loop.output_writes(task.id, task.writes, cached=True)
|
||||
async for _ in runner.atick(
|
||||
loop.tasks.values(),
|
||||
[t for t in loop.tasks.values() if not t.writes],
|
||||
timeout=self.step_timeout,
|
||||
retry_policy=self.retry_policy,
|
||||
get_waiter=get_waiter,
|
||||
match_cached_writes=loop.amatch_cached_writes,
|
||||
):
|
||||
# emit output
|
||||
for o in output():
|
||||
@@ -2922,6 +2995,44 @@ class Pregel(PregelProtocol):
|
||||
else:
|
||||
return chunks
|
||||
|
||||
def clear_cache(self, nodes: Sequence[str] | None = None) -> None:
|
||||
"""Clear the cache for the given nodes."""
|
||||
if not self.cache:
|
||||
raise ValueError("No cache is set for this graph. Cannot clear cache.")
|
||||
nodes = nodes or self.nodes.keys()
|
||||
# collect namespaces to clear
|
||||
namespaces: list[tuple[str, ...]] = []
|
||||
for node in nodes:
|
||||
if node in self.nodes:
|
||||
namespaces.append(
|
||||
(
|
||||
CACHE_NS_WRITES,
|
||||
(identifier(self.nodes[node]) or "__dynamic__"),
|
||||
node,
|
||||
),
|
||||
)
|
||||
# clear cache
|
||||
self.cache.clear(namespaces)
|
||||
|
||||
async def aclear_cache(self, nodes: Sequence[str] | None = None) -> None:
|
||||
"""Asynchronously clear the cache for the given nodes."""
|
||||
if not self.cache:
|
||||
raise ValueError("No cache is set for this graph. Cannot clear cache.")
|
||||
nodes = nodes or self.nodes.keys()
|
||||
# collect namespaces to clear
|
||||
namespaces: list[tuple[str, ...]] = []
|
||||
for node in nodes:
|
||||
if node in self.nodes:
|
||||
namespaces.append(
|
||||
(
|
||||
CACHE_NS_WRITES,
|
||||
(identifier(self.nodes[node]) or "__dynamic__"),
|
||||
node,
|
||||
),
|
||||
)
|
||||
# clear cache
|
||||
await self.cache.aclear(namespaces)
|
||||
|
||||
|
||||
def _trigger_to_nodes(nodes: dict[str, PregelNode]) -> Mapping[str, Sequence[str]]:
|
||||
"""Index from a trigger to nodes that depend on it."""
|
||||
|
||||
@@ -33,6 +33,7 @@ from langgraph.checkpoint.base import (
|
||||
V,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CACHE_NS_WRITES,
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
@@ -65,13 +66,15 @@ from langgraph.constants import (
|
||||
)
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.managed.base import ManagedValueMapping
|
||||
from langgraph.pregel.call import get_runnable_for_task
|
||||
from langgraph.pregel.call import get_runnable_for_task, identifier
|
||||
from langgraph.pregel.io import read_channels
|
||||
from langgraph.pregel.log import logger
|
||||
from langgraph.pregel.read import INPUT_CACHE_KEY_TYPE, PregelNode
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CacheKey,
|
||||
CachePolicy,
|
||||
PregelExecutableTask,
|
||||
PregelScratchpad,
|
||||
PregelTask,
|
||||
@@ -111,24 +114,27 @@ class PregelTaskWrites(NamedTuple):
|
||||
|
||||
|
||||
class Call:
|
||||
__slots__ = ("func", "input", "retry", "callbacks")
|
||||
__slots__ = ("func", "input", "retry", "cache_policy", "callbacks")
|
||||
|
||||
func: Callable
|
||||
input: Any
|
||||
input: tuple[tuple[Any, ...], dict[str, Any]]
|
||||
retry: Optional[Sequence[RetryPolicy]]
|
||||
cache_policy: Optional[CachePolicy]
|
||||
callbacks: Callbacks
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable,
|
||||
input: Any,
|
||||
input: tuple[tuple[Any, ...], dict[str, Any]],
|
||||
*,
|
||||
retry: Optional[Sequence[RetryPolicy]],
|
||||
cache_policy: Optional[CachePolicy],
|
||||
callbacks: Callbacks,
|
||||
) -> None:
|
||||
self.func = func
|
||||
self.input = input
|
||||
self.retry = retry
|
||||
self.cache_policy = cache_policy
|
||||
self.callbacks = callbacks
|
||||
|
||||
|
||||
@@ -232,6 +238,7 @@ def apply_writes(
|
||||
channels: Mapping[str, BaseChannel],
|
||||
tasks: Iterable[WritesProtocol],
|
||||
get_next_version: Optional[GetNextVersion],
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
) -> tuple[dict[str, list[Any]], set[str]]:
|
||||
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
|
||||
to the checkpoint and channels, and return managed values writes to be applied
|
||||
@@ -317,7 +324,9 @@ def apply_writes(
|
||||
max_version,
|
||||
channels[chan],
|
||||
)
|
||||
updated_channels.add(chan)
|
||||
# unavailable channels can't trigger tasks, so don't add them
|
||||
if channels[chan].is_available():
|
||||
updated_channels.add(chan)
|
||||
|
||||
# Channels that weren't updated in this step are notified of a new step
|
||||
if bump_step:
|
||||
@@ -328,6 +337,26 @@ def apply_writes(
|
||||
max_version,
|
||||
channels[chan],
|
||||
)
|
||||
# unavailable channels can't trigger tasks, so don't add them
|
||||
if channels[chan].is_available():
|
||||
updated_channels.add(chan)
|
||||
|
||||
# If this is (tentatively) the last superstep, notify all channels of finish
|
||||
if (
|
||||
bump_step
|
||||
and not checkpoint["pending_sends"]
|
||||
and updated_channels.isdisjoint(trigger_to_nodes)
|
||||
):
|
||||
for chan in channels:
|
||||
if channels[chan].finish() and get_next_version is not None:
|
||||
checkpoint["channel_versions"][chan] = get_next_version(
|
||||
max_version,
|
||||
channels[chan],
|
||||
)
|
||||
# unavailable channels can't trigger tasks, so don't add them
|
||||
if channels[chan].is_available():
|
||||
updated_channels.add(chan)
|
||||
|
||||
# Return managed values writes to be applied externally
|
||||
return pending_writes_by_managed, updated_channels
|
||||
|
||||
@@ -359,6 +388,8 @@ def prepare_next_tasks(
|
||||
manager: Literal[None] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
updated_channels: Optional[set[str]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Literal[None] = None,
|
||||
) -> dict[str, PregelTask]: ...
|
||||
|
||||
|
||||
@@ -378,6 +409,8 @@ def prepare_next_tasks(
|
||||
manager: Union[None, ParentRunManager, AsyncParentRunManager],
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
updated_channels: Optional[set[str]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
) -> dict[str, PregelExecutableTask]: ...
|
||||
|
||||
|
||||
@@ -396,6 +429,8 @@ def prepare_next_tasks(
|
||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
updated_channels: Optional[set[str]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
) -> Union[dict[str, PregelTask], dict[str, PregelExecutableTask]]:
|
||||
"""Prepare the set of tasks that will make up the next Pregel step.
|
||||
|
||||
@@ -446,6 +481,8 @@ def prepare_next_tasks(
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
input_cache=input_cache,
|
||||
cache_policy=cache_policy,
|
||||
retry_policy=retry_policy,
|
||||
):
|
||||
tasks.append(task)
|
||||
|
||||
@@ -489,6 +526,8 @@ def prepare_next_tasks(
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
input_cache=input_cache,
|
||||
cache_policy=cache_policy,
|
||||
retry_policy=retry_policy,
|
||||
):
|
||||
tasks.append(task)
|
||||
return {t.id: t for t in tasks}
|
||||
@@ -515,6 +554,8 @@ def prepare_single_task(
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
||||
input_cache: Optional[dict[INPUT_CACHE_KEY_TYPE, Any]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
) -> Union[None, PregelTask, PregelExecutableTask]:
|
||||
"""Prepares a single task for the next Pregel step, given a task path, which
|
||||
uniquely identifies a PUSH or PULL task within the graph."""
|
||||
@@ -557,6 +598,21 @@ def prepare_single_task(
|
||||
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
||||
if for_execution:
|
||||
writes: deque[tuple[str, Any]] = deque()
|
||||
cache_policy = call.cache_policy or cache_policy
|
||||
if cache_policy:
|
||||
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
|
||||
cache_key: Optional[CacheKey] = CacheKey(
|
||||
(
|
||||
CACHE_NS_WRITES,
|
||||
(identifier(call.func) or "__dynamic__"),
|
||||
),
|
||||
xxh3_128_hexdigest(
|
||||
args_key.encode() if isinstance(args_key, str) else args_key,
|
||||
),
|
||||
cache_policy.ttl,
|
||||
)
|
||||
else:
|
||||
cache_key = None
|
||||
return PregelExecutableTask(
|
||||
name,
|
||||
call.input,
|
||||
@@ -601,8 +657,8 @@ def prepare_single_task(
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
call.retry,
|
||||
None,
|
||||
call.retry or retry_policy,
|
||||
cache_key,
|
||||
task_id,
|
||||
task_path,
|
||||
)
|
||||
@@ -626,6 +682,11 @@ def prepare_single_task(
|
||||
f"Ignoring unknown node name {packet.node} in pending sends"
|
||||
)
|
||||
return
|
||||
# find process
|
||||
proc = processes[packet.node]
|
||||
proc_node = proc.node
|
||||
if proc_node is None:
|
||||
return
|
||||
# create task id
|
||||
triggers = PUSH_TRIGGER
|
||||
checkpoint_ns = (
|
||||
@@ -656,73 +717,80 @@ def prepare_single_task(
|
||||
if task_id_checksum is not None:
|
||||
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
||||
if for_execution:
|
||||
proc = processes[packet.node]
|
||||
if node := proc.node:
|
||||
if proc.metadata:
|
||||
metadata.update(proc.metadata)
|
||||
writes = deque()
|
||||
return PregelExecutableTask(
|
||||
packet.node,
|
||||
packet.arg,
|
||||
node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(
|
||||
config, {"metadata": metadata, "tags": proc.tags}
|
||||
),
|
||||
run_name=packet.node,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}") if manager else None
|
||||
),
|
||||
configurable={
|
||||
CONFIG_KEY_TASK_ID: task_id,
|
||||
# deque.extend is thread-safe
|
||||
CONFIG_KEY_SEND: partial(
|
||||
local_write,
|
||||
writes.extend,
|
||||
processes.keys(),
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
task_path, packet.node, writes, triggers
|
||||
),
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
store or configurable.get(CONFIG_KEY_STORE)
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
checkpointer
|
||||
or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
||||
parent_ns: checkpoint["id"],
|
||||
},
|
||||
CONFIG_KEY_CHECKPOINT_ID: None,
|
||||
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
||||
CONFIG_KEY_SCRATCHPAD: _scratchpad(
|
||||
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
||||
pending_writes,
|
||||
task_id,
|
||||
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
||||
config[CONF].get(CONFIG_KEY_RESUME_MAP),
|
||||
),
|
||||
CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get(
|
||||
PREVIOUS, None
|
||||
),
|
||||
},
|
||||
if proc.metadata:
|
||||
metadata.update(proc.metadata)
|
||||
writes = deque()
|
||||
cache_policy = proc.cache_policy or cache_policy
|
||||
if cache_policy:
|
||||
args_key = cache_policy.key_func(packet.arg)
|
||||
cache_key = CacheKey(
|
||||
(
|
||||
CACHE_NS_WRITES,
|
||||
(identifier(proc) or "__dynamic__"),
|
||||
packet.node,
|
||||
),
|
||||
triggers,
|
||||
proc.retry_policy,
|
||||
None,
|
||||
task_id,
|
||||
task_path,
|
||||
writers=proc.flat_writers,
|
||||
subgraphs=proc.subgraphs,
|
||||
xxh3_128_hexdigest(
|
||||
args_key.encode() if isinstance(args_key, str) else args_key,
|
||||
),
|
||||
cache_policy.ttl,
|
||||
)
|
||||
else:
|
||||
cache_key = None
|
||||
return PregelExecutableTask(
|
||||
packet.node,
|
||||
packet.arg,
|
||||
proc_node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(config, {"metadata": metadata, "tags": proc.tags}),
|
||||
run_name=packet.node,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}") if manager else None
|
||||
),
|
||||
configurable={
|
||||
CONFIG_KEY_TASK_ID: task_id,
|
||||
# deque.extend is thread-safe
|
||||
CONFIG_KEY_SEND: partial(
|
||||
local_write,
|
||||
writes.extend,
|
||||
processes.keys(),
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(task_path, packet.node, writes, triggers),
|
||||
),
|
||||
CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
||||
parent_ns: checkpoint["id"],
|
||||
},
|
||||
CONFIG_KEY_CHECKPOINT_ID: None,
|
||||
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
||||
CONFIG_KEY_SCRATCHPAD: _scratchpad(
|
||||
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
||||
pending_writes,
|
||||
task_id,
|
||||
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
||||
config[CONF].get(CONFIG_KEY_RESUME_MAP),
|
||||
),
|
||||
CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get(
|
||||
PREVIOUS, None
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
proc.retry_policy or retry_policy,
|
||||
cache_key,
|
||||
task_id,
|
||||
task_path,
|
||||
writers=proc.flat_writers,
|
||||
subgraphs=proc.subgraphs,
|
||||
)
|
||||
else:
|
||||
return PregelTask(task_id, packet.node, task_path)
|
||||
elif task_path[0] == PULL:
|
||||
@@ -784,6 +852,24 @@ def prepare_single_task(
|
||||
if proc.metadata:
|
||||
metadata.update(proc.metadata)
|
||||
writes = deque()
|
||||
cache_policy = proc.cache_policy or cache_policy
|
||||
if cache_policy:
|
||||
args_key = cache_policy.key_func(val)
|
||||
cache_key = CacheKey(
|
||||
(
|
||||
CACHE_NS_WRITES,
|
||||
(identifier(proc) or "__dynamic__"),
|
||||
name,
|
||||
),
|
||||
xxh3_128_hexdigest(
|
||||
args_key.encode()
|
||||
if isinstance(args_key, str)
|
||||
else args_key,
|
||||
),
|
||||
cache_policy.ttl,
|
||||
)
|
||||
else:
|
||||
cache_key = None
|
||||
return PregelExecutableTask(
|
||||
name,
|
||||
val,
|
||||
@@ -844,8 +930,8 @@ def prepare_single_task(
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
proc.retry_policy,
|
||||
None,
|
||||
proc.retry_policy or retry_policy,
|
||||
cache_key,
|
||||
task_id,
|
||||
task_path[:3],
|
||||
writers=proc.flat_writers,
|
||||
@@ -977,7 +1063,8 @@ def _proc_input(
|
||||
val = channels[chan].get()
|
||||
break
|
||||
else:
|
||||
val[k] = managed[k]()
|
||||
val = managed[chan]()
|
||||
break
|
||||
else:
|
||||
return MISSING
|
||||
else:
|
||||
@@ -996,18 +1083,20 @@ def _proc_input(
|
||||
return val
|
||||
|
||||
|
||||
def _uuid5_str(namespace: bytes, *parts: str) -> str:
|
||||
def _uuid5_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
||||
"""Generate a UUID from the SHA-1 hash of a namespace and str parts."""
|
||||
|
||||
sha = sha1(namespace, usedforsecurity=False)
|
||||
sha.update(b"".join(p.encode() for p in parts))
|
||||
sha.update(b"".join(p.encode() if isinstance(p, str) else p for p in parts))
|
||||
hex = sha.hexdigest()
|
||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||
|
||||
|
||||
def _xxhash_str(namespace: bytes, *parts: str) -> str:
|
||||
def _xxhash_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
||||
"""Generate a UUID from the XXH3 hash of a namespace and str parts."""
|
||||
hex = xxh3_128_hexdigest(namespace + b"".join(p.encode() for p in parts))
|
||||
hex = xxh3_128_hexdigest(
|
||||
namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
|
||||
)
|
||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing_extensions import ParamSpec
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_CALL, RETURN
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.types import RetryPolicy
|
||||
from langgraph.types import CachePolicy, RetryPolicy
|
||||
from langgraph.utils.config import get_config
|
||||
from langgraph.utils.runnable import (
|
||||
RunnableCallable,
|
||||
@@ -28,6 +28,7 @@ from langgraph.utils.runnable import (
|
||||
|
||||
|
||||
def _getattribute(obj: Any, name: str) -> Any:
|
||||
parent = None
|
||||
for subpath in name.split("."):
|
||||
if subpath == "<locals>":
|
||||
raise AttributeError(f"Can't get local attribute {name!r} on {obj!r}")
|
||||
@@ -73,6 +74,35 @@ def _whichmodule(obj: Any, name: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def identifier(obj: Any, name: Optional[str] = None) -> Optional[str]:
|
||||
"""Return the module and name of an object."""
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
|
||||
|
||||
if isinstance(obj, PregelNode):
|
||||
obj = obj.bound
|
||||
if isinstance(obj, RunnableSeq):
|
||||
obj = obj.steps[0]
|
||||
if isinstance(obj, RunnableCallable):
|
||||
obj = obj.func
|
||||
if name is None:
|
||||
name = getattr(obj, "__qualname__", None)
|
||||
if name is None: # pragma: no cover
|
||||
# This used to be needed for Python 2.7 support but is probably not
|
||||
# needed anymore. However we keep the __name__ introspection in case
|
||||
# users of cloudpickle rely on this old behavior for unknown reasons.
|
||||
name = getattr(obj, "__name__", None)
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
module_name = getattr(obj, "__module__", None)
|
||||
if module_name is None:
|
||||
# In this case, obj.__module__ is None. obj is thus treated as dynamic.
|
||||
return None
|
||||
|
||||
return f"{module_name}.{name}"
|
||||
|
||||
|
||||
def _lookup_module_and_qualname(
|
||||
obj: Any, name: Optional[str] = None
|
||||
) -> Optional[tuple[types.ModuleType, str]]:
|
||||
@@ -135,7 +165,7 @@ def _explode_args_trace_inputs(
|
||||
return arguments
|
||||
|
||||
|
||||
def get_runnable_for_entrypoint(func: Callable[..., Any]) -> RunnableSeq:
|
||||
def get_runnable_for_entrypoint(func: Callable[..., Any]) -> Runnable:
|
||||
key = (func, False)
|
||||
if key in CACHE:
|
||||
return CACHE[key]
|
||||
@@ -160,7 +190,7 @@ def get_runnable_for_entrypoint(func: Callable[..., Any]) -> RunnableSeq:
|
||||
return CACHE.setdefault(key, run)
|
||||
|
||||
|
||||
def get_runnable_for_task(func: Callable[..., Any]) -> RunnableSeq:
|
||||
def get_runnable_for_task(func: Callable[..., Any]) -> Runnable:
|
||||
key = (func, True)
|
||||
if key in CACHE:
|
||||
return CACHE[key]
|
||||
@@ -222,9 +252,16 @@ def call(
|
||||
func: Callable[P, T],
|
||||
*args: Any,
|
||||
retry: Optional[Sequence[RetryPolicy]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
**kwargs: Any,
|
||||
) -> SyncAsyncFuture[T]:
|
||||
config = get_config()
|
||||
impl = config[CONF][CONFIG_KEY_CALL]
|
||||
fut = impl(func, (args, kwargs), retry=retry, callbacks=config["callbacks"])
|
||||
fut = impl(
|
||||
func,
|
||||
(args, kwargs),
|
||||
retry=retry,
|
||||
cache_policy=cache_policy,
|
||||
callbacks=config["callbacks"],
|
||||
)
|
||||
return fut
|
||||
|
||||
@@ -31,9 +31,10 @@ def draw_graph(
|
||||
input_channels: Union[str, Sequence[str]],
|
||||
interrupt_after_nodes: Union[All, Sequence[str]],
|
||||
interrupt_before_nodes: Union[All, Sequence[str]],
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]],
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
checkpointer: Checkpointer,
|
||||
subgraphs: dict[str, Graph],
|
||||
limit: int = 250,
|
||||
) -> Graph:
|
||||
"""Get the graph for this Pregel instance.
|
||||
|
||||
@@ -78,6 +79,7 @@ def draw_graph(
|
||||
PregelTaskWrites((), INPUT, input_writes, []),
|
||||
],
|
||||
get_next_version,
|
||||
trigger_to_nodes,
|
||||
)
|
||||
# prepare first tasks
|
||||
tasks = prepare_next_tasks(
|
||||
@@ -97,7 +99,9 @@ def draw_graph(
|
||||
)
|
||||
start_tasks = tasks
|
||||
# run the pregel loop
|
||||
while tasks:
|
||||
for step in range(step, limit):
|
||||
if not tasks:
|
||||
break
|
||||
conditionals: dict[tuple[str, str, Any], Optional[str]] = {}
|
||||
# run task writers
|
||||
for task in tasks.values():
|
||||
@@ -141,7 +145,7 @@ def draw_graph(
|
||||
trigger_to_sources[trigger].add((src, cond, label))
|
||||
# apply writes
|
||||
_, updated_channels = apply_writes(
|
||||
checkpoint, channels, tasks.values(), get_next_version
|
||||
checkpoint, channels, tasks.values(), get_next_version, trigger_to_nodes
|
||||
)
|
||||
# prepare next tasks
|
||||
tasks = prepare_next_tasks(
|
||||
@@ -161,9 +165,19 @@ def draw_graph(
|
||||
)
|
||||
# collect edges
|
||||
for task in tasks.values():
|
||||
added = False
|
||||
for trigger in task.triggers:
|
||||
for src, cond, label in sorted(trigger_to_sources[trigger]):
|
||||
edges.add((src, task.name, cond, label))
|
||||
# if the edge is from this step, skip adding the implicit edges
|
||||
if (trigger, cond, label) in step_sources.get(src, set()):
|
||||
added = True
|
||||
else:
|
||||
sources[src].discard((trigger, cond, label))
|
||||
# if no edges from this step, add implicit edges from all previous tasks
|
||||
if not added:
|
||||
for src in step_sources:
|
||||
edges.add((src, task.name, True, None))
|
||||
# assemble the graph
|
||||
graph = Graph()
|
||||
# add nodes
|
||||
|
||||
@@ -27,6 +27,7 @@ from langchain_core.runnables import RunnableConfig
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ParamSpec, Self
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
@@ -117,10 +118,12 @@ from langgraph.pregel.utils import get_new_channel_versions, is_xxh3_128_hexdige
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CachePolicy,
|
||||
Command,
|
||||
LoopProtocol,
|
||||
PregelExecutableTask,
|
||||
PregelScratchpad,
|
||||
RetryPolicy,
|
||||
StreamChunk,
|
||||
StreamProtocol,
|
||||
)
|
||||
@@ -133,6 +136,7 @@ INPUT_DONE = object()
|
||||
INPUT_RESUMING = object()
|
||||
INPUT_SHOULD_VALIDATE = object()
|
||||
SPECIAL_CHANNELS = (ERROR, INTERRUPT, SCHEDULED)
|
||||
WritesT = Sequence[tuple[str, Any]]
|
||||
|
||||
|
||||
def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
||||
@@ -147,6 +151,7 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
||||
class PregelLoop(LoopProtocol):
|
||||
input: Optional[Any]
|
||||
input_model: Optional[type[BaseModel]]
|
||||
cache: Optional[BaseCache[WritesT]]
|
||||
checkpointer: Optional[BaseCheckpointSaver]
|
||||
nodes: Mapping[str, PregelNode]
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
@@ -159,18 +164,18 @@ class PregelLoop(LoopProtocol):
|
||||
interrupt_before: Union[All, Sequence[str]]
|
||||
checkpoint_during: bool
|
||||
debug: bool
|
||||
retry_policy: Sequence[RetryPolicy]
|
||||
cache_policy: Optional[CachePolicy]
|
||||
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
checkpointer_put_writes: Optional[
|
||||
Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], Any]
|
||||
]
|
||||
checkpointer_put_writes: Optional[Callable[[RunnableConfig, WritesT, str], Any]]
|
||||
checkpointer_put_writes_accepts_task_path: bool
|
||||
_checkpointer_put_after_previous: Optional[
|
||||
Callable[
|
||||
[
|
||||
Optional[concurrent.futures.Future],
|
||||
RunnableConfig,
|
||||
Sequence[tuple[str, Any]],
|
||||
Checkpoint,
|
||||
str,
|
||||
ChannelVersions,
|
||||
],
|
||||
@@ -206,18 +211,21 @@ class PregelLoop(LoopProtocol):
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
cache: Optional[BaseCache],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
nodes: Mapping[str, PregelNode],
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
output_keys: Union[str, Sequence[str]],
|
||||
stream_keys: Union[str, Sequence[str]],
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
input_model: Optional[type[BaseModel]] = None,
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
checkpoint_during: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -230,6 +238,7 @@ class PregelLoop(LoopProtocol):
|
||||
self.input = input
|
||||
self.input_model = input_model
|
||||
self.checkpointer = checkpointer
|
||||
self.cache = cache
|
||||
self.nodes = nodes
|
||||
self.specs = specs
|
||||
self.output_keys = output_keys
|
||||
@@ -244,6 +253,8 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
self._migrate_checkpoint = migrate_checkpoint
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.retry_policy = retry_policy
|
||||
self.cache_policy = cache_policy
|
||||
self.checkpoint_during = checkpoint_during
|
||||
self.debug = debug
|
||||
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
||||
@@ -299,7 +310,7 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
self.prev_checkpoint_config = None
|
||||
|
||||
def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None:
|
||||
def put_writes(self, task_id: str, writes: WritesT) -> None:
|
||||
"""Put writes for a task, to be read by the next tick."""
|
||||
if not writes:
|
||||
return
|
||||
@@ -346,7 +357,7 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
# output writes
|
||||
if hasattr(self, "tasks"):
|
||||
self._output_writes(task_id, writes)
|
||||
self.output_writes(task_id, writes)
|
||||
|
||||
def _put_pending_writes(self) -> None:
|
||||
if self.checkpointer_put_writes is None:
|
||||
@@ -418,6 +429,8 @@ class PregelLoop(LoopProtocol):
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer,
|
||||
manager=self.manager,
|
||||
retry_policy=self.retry_policy,
|
||||
cache_policy=self.cache_policy,
|
||||
),
|
||||
):
|
||||
# don't start if we should interrupt *before* the new task
|
||||
@@ -483,6 +496,7 @@ class PregelLoop(LoopProtocol):
|
||||
self.channels,
|
||||
self.tasks.values(),
|
||||
self.checkpointer_get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# apply writes to managed values
|
||||
for key, values in mv_writes.items():
|
||||
@@ -546,6 +560,8 @@ class PregelLoop(LoopProtocol):
|
||||
checkpointer=self.checkpointer,
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
updated_channels=updated_channels,
|
||||
retry_policy=self.retry_policy,
|
||||
cache_policy=self.cache_policy,
|
||||
)
|
||||
self.to_interrupt = []
|
||||
|
||||
@@ -608,10 +624,16 @@ class PregelLoop(LoopProtocol):
|
||||
# print output for any tasks we applied previous writes to
|
||||
for task in self.tasks.values():
|
||||
if task.writes:
|
||||
self._output_writes(task.id, task.writes, cached=True)
|
||||
self.output_writes(task.id, task.writes, cached=True)
|
||||
|
||||
return True
|
||||
|
||||
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def amatch_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
raise NotImplementedError
|
||||
|
||||
# private
|
||||
|
||||
def _match_writes(self, tasks: Mapping[str, PregelExecutableTask]) -> None:
|
||||
@@ -679,6 +701,7 @@ class PregelLoop(LoopProtocol):
|
||||
self.channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
self.checkpointer_get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
for key, values in mv_writes.items():
|
||||
self._update_mv(key, values)
|
||||
@@ -731,6 +754,7 @@ class PregelLoop(LoopProtocol):
|
||||
PregelTaskWrites((), INPUT, input_writes, []),
|
||||
],
|
||||
self.checkpointer_get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
assert not mv_writes, "Can't write to SharedValues in graph input"
|
||||
# save input checkpoint
|
||||
@@ -868,6 +892,7 @@ class PregelLoop(LoopProtocol):
|
||||
self.channels,
|
||||
self.tasks.values(),
|
||||
self.checkpointer_get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
for key, values in mv_writes.items():
|
||||
self._update_mv(key, values)
|
||||
@@ -908,8 +933,8 @@ class PregelLoop(LoopProtocol):
|
||||
for v in values(*args, **kwargs):
|
||||
self.stream((self.checkpoint_ns, mode, v))
|
||||
|
||||
def _output_writes(
|
||||
self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False
|
||||
def output_writes(
|
||||
self, task_id: str, writes: WritesT, *, cached: bool = False
|
||||
) -> None:
|
||||
if task := self.tasks.get(task_id):
|
||||
if task.config is not None and TAG_HIDDEN in task.config.get(
|
||||
@@ -963,9 +988,11 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
cache: Optional[BaseCache],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
nodes: Mapping[str, PregelNode],
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
@@ -974,7 +1001,8 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
input_model: Optional[type[BaseModel]] = None,
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
checkpoint_during: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -983,6 +1011,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
stream=stream,
|
||||
config=config,
|
||||
checkpointer=checkpointer,
|
||||
cache=cache,
|
||||
store=store,
|
||||
nodes=nodes,
|
||||
specs=specs,
|
||||
@@ -994,6 +1023,8 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
debug=debug,
|
||||
migrate_checkpoint=migrate_checkpoint,
|
||||
trigger_to_nodes=trigger_to_nodes,
|
||||
retry_policy=retry_policy,
|
||||
cache_policy=cache_policy,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
self.stack = ExitStack()
|
||||
@@ -1033,6 +1064,39 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
|
||||
return self.submit(cast(WritableManagedValue, managed_value).update, values)
|
||||
|
||||
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
if self.cache is None:
|
||||
return ()
|
||||
matched: list[PregelExecutableTask] = []
|
||||
if cached := {
|
||||
(t.cache_key.ns, t.cache_key.key): t
|
||||
for t in self.tasks.values()
|
||||
if t.cache_key and not t.writes
|
||||
}:
|
||||
for key, values in self.cache.get(tuple(cached)).items():
|
||||
task = cached[key]
|
||||
task.writes.extend(values)
|
||||
matched.append(task)
|
||||
return matched
|
||||
|
||||
def put_writes(self, task_id: str, writes: WritesT) -> None:
|
||||
"""Put writes for a task, to be read by the next tick."""
|
||||
super().put_writes(task_id, writes)
|
||||
if not writes or self.cache is None or not hasattr(self, "tasks"):
|
||||
return
|
||||
task = self.tasks.get(task_id)
|
||||
if task is None or task.cache_key is None:
|
||||
return
|
||||
self.submit(
|
||||
self.cache.set,
|
||||
{
|
||||
(task.cache_key.ns, task.cache_key.key): (
|
||||
task.writes,
|
||||
task.cache_key.ttl,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# context manager
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
@@ -1113,9 +1177,11 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
cache: Optional[BaseCache],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
nodes: Mapping[str, PregelNode],
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
@@ -1124,7 +1190,8 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
input_model: Optional[type[BaseModel]] = None,
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
retry_policy: Sequence[RetryPolicy] = (),
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
checkpoint_during: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -1133,6 +1200,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
stream=stream,
|
||||
config=config,
|
||||
checkpointer=checkpointer,
|
||||
cache=cache,
|
||||
store=store,
|
||||
nodes=nodes,
|
||||
specs=specs,
|
||||
@@ -1144,6 +1212,8 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
debug=debug,
|
||||
migrate_checkpoint=migrate_checkpoint,
|
||||
trigger_to_nodes=trigger_to_nodes,
|
||||
retry_policy=retry_policy,
|
||||
cache_policy=cache_policy,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
self.stack = AsyncExitStack()
|
||||
@@ -1183,6 +1253,42 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
|
||||
return self.submit(cast(WritableManagedValue, managed_value).aupdate, values)
|
||||
|
||||
async def amatch_cached_writes(self) -> Sequence[PregelExecutableTask]:
|
||||
if self.cache is None:
|
||||
return []
|
||||
matched: list[PregelExecutableTask] = []
|
||||
if cached := {
|
||||
(t.cache_key.ns, t.cache_key.key): t
|
||||
for t in self.tasks.values()
|
||||
if t.cache_key and not t.writes
|
||||
}:
|
||||
for key, values in (await self.cache.aget(tuple(cached))).items():
|
||||
task = cached[key]
|
||||
task.writes.extend(values)
|
||||
matched.append(task)
|
||||
return matched
|
||||
|
||||
def put_writes(self, task_id: str, writes: WritesT) -> None:
|
||||
"""Put writes for a task, to be read by the next tick."""
|
||||
super().put_writes(task_id, writes)
|
||||
if not writes or self.cache is None or not hasattr(self, "tasks"):
|
||||
return
|
||||
task = self.tasks.get(task_id)
|
||||
if task is None or task.cache_key is None:
|
||||
return
|
||||
if writes[0][0] in (INTERRUPT, ERROR):
|
||||
# only cache successful tasks
|
||||
return
|
||||
self.submit(
|
||||
self.cache.aset,
|
||||
{
|
||||
(task.cache_key.ns, task.cache_key.key): (
|
||||
task.writes,
|
||||
task.cache_key.ttl,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# context manager
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
|
||||
@@ -14,14 +14,15 @@ from langchain_core.runnables import (
|
||||
RunnablePassthrough,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Input, Other, coerce_to_runnable
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec
|
||||
from langchain_core.runnables.base import Other, coerce_to_runnable
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec, Input
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_READ
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.retry import RetryPolicy
|
||||
from langgraph.pregel.utils import find_subgraph_pregel
|
||||
from langgraph.pregel.write import ChannelWrite
|
||||
from langgraph.types import CachePolicy
|
||||
from langgraph.utils.config import merge_configs
|
||||
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
|
||||
|
||||
@@ -143,6 +144,9 @@ class PregelNode(Runnable):
|
||||
retry_policy: Sequence[RetryPolicy] | None
|
||||
"""The retry policies to use when invoking the node."""
|
||||
|
||||
cache_policy: CachePolicy | None
|
||||
"""The cache policy to use when invoking the node."""
|
||||
|
||||
tags: Sequence[str] | None
|
||||
"""Tags to attach to the node for tracing."""
|
||||
|
||||
@@ -163,6 +167,7 @@ class PregelNode(Runnable):
|
||||
metadata: Mapping[str, Any] | None = None,
|
||||
bound: Runnable[Any, Any] | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||
cache_policy: CachePolicy | None = None,
|
||||
subgraphs: Sequence[PregelProtocol] | None = None,
|
||||
) -> None:
|
||||
self.channels = channels
|
||||
@@ -170,8 +175,9 @@ class PregelNode(Runnable):
|
||||
self.mapper = mapper
|
||||
self.writers = writers or []
|
||||
self.bound = bound if bound is not None else DEFAULT_BOUND
|
||||
self.cache_policy = cache_policy
|
||||
if isinstance(retry_policy, RetryPolicy):
|
||||
self.retry_policy: Sequence[RetryPolicy] = (retry_policy,)
|
||||
self.retry_policy = (retry_policy,)
|
||||
else:
|
||||
self.retry_policy = retry_policy
|
||||
self.tags = tags
|
||||
|
||||
@@ -36,6 +36,7 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_STREAM,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
INTERRUPT,
|
||||
NS_SEP,
|
||||
)
|
||||
@@ -50,6 +51,7 @@ CONF_DROPLIST = frozenset(
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -93,11 +95,12 @@ class RemoteGraph(PregelProtocol):
|
||||
a node in another `Graph`.
|
||||
"""
|
||||
|
||||
name: str
|
||||
assistant_id: str
|
||||
name: Optional[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str, # graph_id
|
||||
assistant_id: str, # graph_id
|
||||
/,
|
||||
*,
|
||||
url: Optional[str] = None,
|
||||
@@ -106,6 +109,7 @@ class RemoteGraph(PregelProtocol):
|
||||
client: Optional[LangGraphClient] = None,
|
||||
sync_client: Optional[SyncLangGraphClient] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
"""Specify `url`, `api_key`, and/or `headers` to create default sync and async clients.
|
||||
|
||||
@@ -114,15 +118,22 @@ class RemoteGraph(PregelProtocol):
|
||||
one of `url`, `client`, or `sync_client` must be provided.
|
||||
|
||||
Args:
|
||||
name: The name of the graph.
|
||||
assistant_id: The assistant ID or graph name of the remote graph to use.
|
||||
url: The URL of the remote API.
|
||||
api_key: The API key to use for authentication. If not provided, it will be read from the environment (`LANGGRAPH_API_KEY`, `LANGSMITH_API_KEY`, or `LANGCHAIN_API_KEY`).
|
||||
headers: Additional headers to include in the requests.
|
||||
client: A `LangGraphClient` instance to use instead of creating a default client.
|
||||
sync_client: A `SyncLangGraphClient` instance to use instead of creating a default client.
|
||||
config: An optional `RunnableConfig` instance with additional configuration.
|
||||
name: Human-readable name to attach to the RemoteGraph instance.
|
||||
This is useful for adding `RemoteGraph` as a subgraph via `graph.add_node(remote_graph)`.
|
||||
If not provided, defaults to the assistant ID.
|
||||
"""
|
||||
self.name = name
|
||||
self.assistant_id = assistant_id
|
||||
if name is None:
|
||||
self.name = assistant_id
|
||||
else:
|
||||
self.name = name
|
||||
self.config = config
|
||||
|
||||
if client is None and url is not None:
|
||||
@@ -149,7 +160,7 @@ class RemoteGraph(PregelProtocol):
|
||||
|
||||
def copy(self, update: dict[str, Any]) -> Self:
|
||||
attrs = {**self.__dict__, **update}
|
||||
return self.__class__(attrs.pop("name"), **attrs)
|
||||
return self.__class__(attrs.pop("assistant_id"), **attrs)
|
||||
|
||||
def with_config(
|
||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
@@ -203,7 +214,7 @@ class RemoteGraph(PregelProtocol):
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
graph = sync_client.assistants.get_graph(
|
||||
assistant_id=self.name,
|
||||
assistant_id=self.assistant_id,
|
||||
xray=xray,
|
||||
)
|
||||
return DrawableGraph(
|
||||
@@ -232,7 +243,7 @@ class RemoteGraph(PregelProtocol):
|
||||
"""
|
||||
client = self._validate_client()
|
||||
graph = await client.assistants.get_graph(
|
||||
assistant_id=self.name,
|
||||
assistant_id=self.assistant_id,
|
||||
xray=xray,
|
||||
)
|
||||
return DrawableGraph(
|
||||
@@ -254,11 +265,15 @@ class RemoteGraph(PregelProtocol):
|
||||
path=tuple(),
|
||||
error=Exception(task["error"]) if task["error"] else None,
|
||||
interrupts=tuple(interrupts),
|
||||
state=self._create_state_snapshot(task["state"])
|
||||
if task["state"]
|
||||
else cast(RunnableConfig, {"configurable": task["checkpoint"]})
|
||||
if task["checkpoint"]
|
||||
else None,
|
||||
state=(
|
||||
self._create_state_snapshot(task["state"])
|
||||
if task["state"]
|
||||
else (
|
||||
cast(RunnableConfig, {"configurable": task["checkpoint"]})
|
||||
if task["checkpoint"]
|
||||
else None
|
||||
)
|
||||
),
|
||||
result=task.get("result"),
|
||||
)
|
||||
)
|
||||
@@ -276,18 +291,20 @@ class RemoteGraph(PregelProtocol):
|
||||
},
|
||||
metadata=CheckpointMetadata(**state["metadata"]),
|
||||
created_at=state["created_at"],
|
||||
parent_config={
|
||||
"configurable": {
|
||||
"thread_id": state["parent_checkpoint"]["thread_id"],
|
||||
"checkpoint_ns": state["parent_checkpoint"]["checkpoint_ns"],
|
||||
"checkpoint_id": state["parent_checkpoint"]["checkpoint_id"],
|
||||
"checkpoint_map": state["parent_checkpoint"].get(
|
||||
"checkpoint_map", {}
|
||||
),
|
||||
parent_config=(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": state["parent_checkpoint"]["thread_id"],
|
||||
"checkpoint_ns": state["parent_checkpoint"]["checkpoint_ns"],
|
||||
"checkpoint_id": state["parent_checkpoint"]["checkpoint_id"],
|
||||
"checkpoint_map": state["parent_checkpoint"].get(
|
||||
"checkpoint_map", {}
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
if state["parent_checkpoint"]
|
||||
else None,
|
||||
if state["parent_checkpoint"]
|
||||
else None
|
||||
),
|
||||
tasks=tuple(tasks),
|
||||
interrupts=tuple([i for task in tasks for i in task.interrupts]),
|
||||
)
|
||||
@@ -642,7 +659,7 @@ class RemoteGraph(PregelProtocol):
|
||||
|
||||
for chunk in sync_client.runs.stream(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
assistant_id=self.assistant_id,
|
||||
input=input,
|
||||
command=command,
|
||||
config=sanitized_config,
|
||||
@@ -678,6 +695,10 @@ class RemoteGraph(PregelProtocol):
|
||||
# filter for what was actually requested
|
||||
if mode not in requested:
|
||||
continue
|
||||
|
||||
if chunk.event.startswith("messages"):
|
||||
chunk = chunk._replace(data=tuple(chunk.data)) # type: ignore
|
||||
|
||||
# emit chunk
|
||||
if subgraphs:
|
||||
if NS_SEP in chunk.event:
|
||||
@@ -737,7 +758,7 @@ class RemoteGraph(PregelProtocol):
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
assistant_id=self.assistant_id,
|
||||
input=input,
|
||||
command=command,
|
||||
config=sanitized_config,
|
||||
@@ -773,6 +794,10 @@ class RemoteGraph(PregelProtocol):
|
||||
# filter for what was actually requested
|
||||
if mode not in requested:
|
||||
continue
|
||||
|
||||
if chunk.event.startswith("messages"):
|
||||
chunk = chunk._replace(data=tuple(chunk.data)) # type: ignore
|
||||
|
||||
# emit chunk
|
||||
if subgraphs:
|
||||
if NS_SEP in chunk.event:
|
||||
|
||||
@@ -3,9 +3,9 @@ import logging
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Awaitable, Sequence
|
||||
from dataclasses import replace
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
@@ -61,7 +61,7 @@ def run_with_retry(
|
||||
except Exception as exc:
|
||||
if SUPPORTS_EXC_NOTES:
|
||||
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
|
||||
if retry_policy is None:
|
||||
if not retry_policy:
|
||||
raise
|
||||
|
||||
# Check which retry policy applies to this exception
|
||||
@@ -106,6 +106,9 @@ async def arun_with_retry(
|
||||
task: PregelExecutableTask,
|
||||
retry_policies: Optional[Sequence[RetryPolicy]],
|
||||
stream: bool = False,
|
||||
match_cached_writes: Optional[
|
||||
Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
|
||||
] = None,
|
||||
configurable: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Run a task asynchronously with retries."""
|
||||
@@ -114,6 +117,11 @@ async def arun_with_retry(
|
||||
config = task.config
|
||||
if configurable is not None:
|
||||
config = patch_configurable(config, configurable)
|
||||
if match_cached_writes is not None and task.cache_key is not None:
|
||||
for t in await match_cached_writes():
|
||||
if t is task:
|
||||
# if the task is already cached, return
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
# clear any writes from previous attempts
|
||||
@@ -149,7 +157,7 @@ async def arun_with_retry(
|
||||
except Exception as exc:
|
||||
if SUPPORTS_EXC_NOTES:
|
||||
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
|
||||
if retry_policies is None:
|
||||
if not retry_policies:
|
||||
raise
|
||||
|
||||
# Check which retry policy applies to this exception
|
||||
|
||||
@@ -33,7 +33,12 @@ from langgraph.errors import GraphBubbleUp, GraphInterrupt
|
||||
from langgraph.pregel.algo import Call
|
||||
from langgraph.pregel.executor import Submit
|
||||
from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
from langgraph.types import PregelExecutableTask, PregelScratchpad, RetryPolicy
|
||||
from langgraph.types import (
|
||||
CachePolicy,
|
||||
PregelExecutableTask,
|
||||
PregelScratchpad,
|
||||
RetryPolicy,
|
||||
)
|
||||
from langgraph.utils.future import chain_future
|
||||
|
||||
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
|
||||
@@ -137,6 +142,9 @@ class PregelRunner:
|
||||
timeout: Optional[float] = None,
|
||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
||||
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
|
||||
match_cached_writes: Optional[
|
||||
Callable[[], Sequence[PregelExecutableTask]]
|
||||
] = None,
|
||||
) -> Iterator[None]:
|
||||
tasks = tuple(tasks)
|
||||
futures = FuturesDict(
|
||||
@@ -147,7 +155,9 @@ class PregelRunner:
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no timeout and no waiter
|
||||
if len(tasks) == 1 and timeout is None and get_waiter is None:
|
||||
if len(tasks) == 0:
|
||||
return
|
||||
elif len(tasks) == 1 and timeout is None and get_waiter is None:
|
||||
t = tasks[0]
|
||||
try:
|
||||
run_with_retry(
|
||||
@@ -160,6 +170,7 @@ class PregelRunner:
|
||||
retry=retry_policy,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
),
|
||||
@@ -191,25 +202,25 @@ class PregelRunner:
|
||||
futures[get_waiter()] = None
|
||||
# schedule tasks
|
||||
for t in tasks:
|
||||
if not t.writes:
|
||||
fut = self.submit()( # type: ignore[misc]
|
||||
run_with_retry,
|
||||
t,
|
||||
retry_policy,
|
||||
configurable={
|
||||
CONFIG_KEY_CALL: partial(
|
||||
_call,
|
||||
weakref.ref(t),
|
||||
retry=retry_policy,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
),
|
||||
},
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
futures[fut] = t
|
||||
fut = self.submit()( # type: ignore[misc]
|
||||
run_with_retry,
|
||||
t,
|
||||
retry_policy,
|
||||
configurable={
|
||||
CONFIG_KEY_CALL: partial(
|
||||
_call,
|
||||
weakref.ref(t),
|
||||
retry=retry_policy,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
),
|
||||
},
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
@@ -266,6 +277,9 @@ class PregelRunner:
|
||||
timeout: Optional[float] = None,
|
||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
||||
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
|
||||
match_cached_writes: Optional[
|
||||
Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
|
||||
] = None,
|
||||
) -> AsyncIterator[None]:
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = tuple(tasks)
|
||||
@@ -277,7 +291,9 @@ class PregelRunner:
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no waiter and no timeout
|
||||
if len(tasks) == 1 and get_waiter is None and timeout is None:
|
||||
if len(tasks) == 0:
|
||||
return
|
||||
elif len(tasks) == 1 and get_waiter is None and timeout is None:
|
||||
t = tasks[0]
|
||||
try:
|
||||
await arun_with_retry(
|
||||
@@ -292,6 +308,7 @@ class PregelRunner:
|
||||
retry=retry_policy,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
loop=loop,
|
||||
@@ -324,33 +341,33 @@ class PregelRunner:
|
||||
futures[get_waiter()] = None
|
||||
# schedule tasks
|
||||
for t in tasks:
|
||||
if not t.writes:
|
||||
fut = cast(
|
||||
asyncio.Future,
|
||||
self.submit()( # type: ignore[misc]
|
||||
arun_with_retry,
|
||||
t,
|
||||
retry_policy,
|
||||
stream=self.use_astream,
|
||||
configurable={
|
||||
CONFIG_KEY_CALL: partial(
|
||||
_acall,
|
||||
weakref.ref(t),
|
||||
retry=retry_policy,
|
||||
stream=self.use_astream,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
loop=loop,
|
||||
),
|
||||
},
|
||||
__name__=t.name,
|
||||
__cancel_on_exit__=True,
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
futures[fut] = t
|
||||
fut = cast(
|
||||
asyncio.Future,
|
||||
self.submit()( # type: ignore[misc]
|
||||
arun_with_retry,
|
||||
t,
|
||||
retry_policy,
|
||||
stream=self.use_astream,
|
||||
configurable={
|
||||
CONFIG_KEY_CALL: partial(
|
||||
_acall,
|
||||
weakref.ref(t),
|
||||
retry=retry_policy,
|
||||
stream=self.use_astream,
|
||||
futures=weakref.ref(futures),
|
||||
schedule_task=self.schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=self.submit,
|
||||
reraise=reraise,
|
||||
loop=loop,
|
||||
),
|
||||
},
|
||||
__name__=t.name,
|
||||
__cancel_on_exit__=True,
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
@@ -515,6 +532,7 @@ def _call(
|
||||
input: Any,
|
||||
*,
|
||||
retry: Optional[Sequence[RetryPolicy]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
callbacks: Callbacks = None,
|
||||
futures: weakref.ref[FuturesDict],
|
||||
schedule_task: weakref.ref[
|
||||
@@ -522,6 +540,7 @@ def _call(
|
||||
[PregelExecutableTask, int, Optional[Call]], Optional[PregelExecutableTask]
|
||||
]
|
||||
],
|
||||
match_cached_writes: Optional[Callable[[], Sequence[PregelExecutableTask]]],
|
||||
submit: weakref.ref[Submit],
|
||||
reraise: bool,
|
||||
) -> concurrent.futures.Future[Any]:
|
||||
@@ -535,8 +554,10 @@ def _call(
|
||||
if next_task := schedule_task()( # type: ignore[misc]
|
||||
task(), # type: ignore[arg-type]
|
||||
scratchpad.call_counter(),
|
||||
Call(func, input, retry=retry, callbacks=callbacks),
|
||||
Call(func, input, retry=retry, cache_policy=cache_policy, callbacks=callbacks),
|
||||
):
|
||||
if match_cached_writes:
|
||||
match_cached_writes()
|
||||
if fut := next(
|
||||
(
|
||||
f
|
||||
@@ -574,6 +595,7 @@ def _call(
|
||||
retry=retry,
|
||||
callbacks=callbacks,
|
||||
schedule_task=schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=submit,
|
||||
reraise=reraise,
|
||||
),
|
||||
@@ -596,6 +618,7 @@ def _acall(
|
||||
input: Any,
|
||||
*,
|
||||
retry: Optional[Sequence[RetryPolicy]] = None,
|
||||
cache_policy: Optional[CachePolicy] = None,
|
||||
callbacks: Callbacks = None,
|
||||
# injected dependencies
|
||||
futures: weakref.ref[FuturesDict],
|
||||
@@ -604,6 +627,9 @@ def _acall(
|
||||
[PregelExecutableTask, int, Optional[Call]], Optional[PregelExecutableTask]
|
||||
]
|
||||
],
|
||||
match_cached_writes: Optional[
|
||||
Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
|
||||
] = None,
|
||||
submit: weakref.ref[Submit],
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
reraise: bool = False,
|
||||
@@ -616,7 +642,7 @@ def _acall(
|
||||
if next_task := schedule_task()( # type: ignore[misc]
|
||||
task(), # type: ignore[arg-type]
|
||||
scratchpad.call_counter(),
|
||||
Call(func, input, retry=retry, callbacks=callbacks),
|
||||
Call(func, input, retry=retry, cache_policy=cache_policy, callbacks=callbacks),
|
||||
):
|
||||
if fut := next(
|
||||
(
|
||||
@@ -652,6 +678,7 @@ def _acall(
|
||||
next_task,
|
||||
retry,
|
||||
stream=stream,
|
||||
match_cached_writes=match_cached_writes,
|
||||
configurable={
|
||||
CONFIG_KEY_CALL: partial(
|
||||
_acall,
|
||||
@@ -659,6 +686,7 @@ def _acall(
|
||||
stream=stream,
|
||||
futures=futures,
|
||||
schedule_task=schedule_task,
|
||||
match_cached_writes=match_cached_writes,
|
||||
submit=submit,
|
||||
loop=loop,
|
||||
reraise=reraise,
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing_extensions import Self
|
||||
from xxhash import xxh3_128_hexdigest
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
|
||||
from langgraph.utils.cache import default_cache_key
|
||||
from langgraph.utils.fields import get_update_as_tuples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -122,13 +123,19 @@ class RetryPolicy(NamedTuple):
|
||||
"""List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry."""
|
||||
|
||||
|
||||
class CachePolicy(NamedTuple):
|
||||
"""Configuration for caching nodes.
|
||||
KeyFuncT = TypeVar("KeyFuncT", bound=Callable[..., Union[str, bytes]])
|
||||
|
||||
!!! version-added "Added in version 0.2.24."
|
||||
"""
|
||||
|
||||
pass
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class CachePolicy(Generic[KeyFuncT]):
|
||||
"""Configuration for caching nodes."""
|
||||
|
||||
key_func: KeyFuncT = default_cache_key # type: ignore[assignment]
|
||||
"""Function to generate a cache key from the node's input.
|
||||
Defaults to hashing the input with pickle."""
|
||||
|
||||
ttl: Optional[int] = None
|
||||
"""Time to live for the cache entry in seconds. If None, the entry never expires."""
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
@@ -174,6 +181,17 @@ else:
|
||||
_T_DC_KWARGS = {"frozen": True}
|
||||
|
||||
|
||||
class CacheKey(NamedTuple):
|
||||
"""Cache key for a task."""
|
||||
|
||||
ns: tuple[str, ...]
|
||||
"""Namespace for the cache entry."""
|
||||
key: str
|
||||
"""Key for the cache entry."""
|
||||
ttl: Optional[int]
|
||||
"""Time to live for the cache entry in seconds."""
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_T_DC_KWARGS)
|
||||
class PregelExecutableTask:
|
||||
name: str
|
||||
@@ -182,8 +200,8 @@ class PregelExecutableTask:
|
||||
writes: deque[tuple[str, Any]]
|
||||
config: RunnableConfig
|
||||
triggers: Sequence[str]
|
||||
retry_policy: Optional[Sequence[RetryPolicy]]
|
||||
cache_policy: Optional[CachePolicy]
|
||||
retry_policy: Sequence[RetryPolicy]
|
||||
cache_key: Optional[CacheKey]
|
||||
id: str
|
||||
path: tuple[Union[str, int, tuple], ...]
|
||||
scheduled: bool = False
|
||||
@@ -314,7 +332,7 @@ class Command(Generic[N], ToolOutputMixin):
|
||||
graph: Optional[str] = None
|
||||
update: Optional[Any] = None
|
||||
resume: Optional[Union[dict[str, Any], Any]] = None
|
||||
goto: Union[Send, Sequence[Union[Send, str]], str] = ()
|
||||
goto: Union[Send, Sequence[Union[Send, N]], N] = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# get all non-None values
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Hashable, Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _freeze(obj: Any, depth: int = 10) -> Hashable:
|
||||
if isinstance(obj, Hashable) or depth <= 0:
|
||||
# already hashable, no need to freeze
|
||||
return obj
|
||||
elif isinstance(obj, Mapping):
|
||||
# sort keys so {"a":1,"b":2} == {"b":2,"a":1}
|
||||
return tuple(sorted((k, _freeze(v, depth - 1)) for k, v in obj.items()))
|
||||
elif isinstance(obj, Sequence):
|
||||
return tuple(_freeze(x, depth - 1) for x in obj)
|
||||
# numpy / pandas etc. can provide their own .tobytes()
|
||||
elif hasattr(obj, "tobytes"):
|
||||
return (
|
||||
type(obj).__name__,
|
||||
obj.tobytes(),
|
||||
obj.shape if hasattr(obj, "shape") else None,
|
||||
)
|
||||
return obj # strings, ints, dataclasses with frozen=True, etc.
|
||||
|
||||
|
||||
def default_cache_key(*args: Any, **kwargs: Any) -> str | bytes:
|
||||
"""Default cache key function that uses the arguments and keyword arguments to generate a hashable key."""
|
||||
import pickle
|
||||
|
||||
# protocol 5 strikes a good balance between speed and size
|
||||
return pickle.dumps((_freeze(args), _freeze(kwargs)), protocol=5, fix_imports=False)
|
||||
Generated
+440
-4
@@ -30,6 +30,7 @@ files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
markers = {dev = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
@@ -255,6 +256,22 @@ webencodings = "*"
|
||||
[package.extras]
|
||||
css = ["tinycss2 (>=1.1.0,<1.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "blockbuster"
|
||||
version = "1.5.24"
|
||||
description = "Utility to detect blocking calls in the async event loop"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "blockbuster-1.5.24-py3-none-any.whl", hash = "sha256:e703497b55bc72af09d60d1cd746c2f3ba7ce0c446fa256be6ccda5e7d403520"},
|
||||
{file = "blockbuster-1.5.24.tar.gz", hash = "sha256:97645775761a5d425666ec0bc99629b65c7eccdc2f770d2439850682567af4ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
forbiddenfruit = {version = ">=0.1.4", markers = "implementation_name == \"cpython\""}
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
@@ -463,6 +480,34 @@ files = [
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
|
||||
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "cloudpickle"
|
||||
version = "3.1.1"
|
||||
description = "Pickler class to extend the standard pickle.Pickler functionality"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e"},
|
||||
{file = "cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -470,7 +515,7 @@ description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
markers = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
@@ -572,6 +617,67 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
|
||||
[package.extras]
|
||||
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.3"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
|
||||
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
version = "1.8.7"
|
||||
@@ -693,6 +799,18 @@ files = [
|
||||
[package.extras]
|
||||
devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
|
||||
|
||||
[[package]]
|
||||
name = "forbiddenfruit"
|
||||
version = "0.1.4"
|
||||
description = "Patch python built-in objects"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
markers = "python_version >= \"3.11\" and implementation_name == \"cpython\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fqdn"
|
||||
version = "1.4.0"
|
||||
@@ -700,7 +818,7 @@ description = "Validate fully-qualified domain names compliant to RFC 1035 and t
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
markers = "python_version >= \"3.13\""
|
||||
markers = "python_version >= \"4.0\""
|
||||
files = [
|
||||
{file = "fqdn-1.4.0-py3-none-any.whl", hash = "sha256:e935616ae81c9c60a22267593fe8e6af68cecc68549cc71bb9bfbcbbcb383386"},
|
||||
{file = "fqdn-1.4.0.tar.gz", hash = "sha256:30e8f2e685ce87cdace4712fd97c5d09f5e6fa519bbb66e8f188f6a7cb3a5c4e"},
|
||||
@@ -713,7 +831,7 @@ description = "Validates fully-qualified domain names against RFC 1123, so that
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.13\""
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"},
|
||||
{file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"},
|
||||
@@ -1048,6 +1166,64 @@ webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format
|
||||
format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
|
||||
format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-rs"
|
||||
version = "0.29.1"
|
||||
description = "A high-performance JSON Schema validator for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7d84c4e1483a103694150b5706d638606443c814662738f14d34ca16948349df"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0e7b72365ae0875c0e99f951ad4cec00c6f5e57b25ed5b8495b8f2c810ad21a6"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7078d3cc635b9832ba282ec4de3af4cdaba4af74691e52aa3ea5f7d1baaa3b76"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ed43c64e0d732edf32a881f0c1db340c9484f3a28c41f7da96667a49eb0f34"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a14806090a5ebf1616a0047eb8049f70f3a830c460e71d5f4a78b300644ec9"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-win32.whl", hash = "sha256:3d739419212e87219e0aa5b9b81eee726e755f606ac63f4795e37efeb9635ed9"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8fa9007e76cea86877165ebb13ed94648246a185d5eabaf9125e97636bc56e4"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b4458f1a027ab0c64e91edcb23c48220d60a503e741030bcf260fbbe12979ad2"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:faf3d90b5473bf654fd6ffb490bd6fdd2e54f4034f652d1749bee963b3104ce3"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e96919483960737ea5cd8d36e0752c63b875459f31ae14b3a6e80df925b74947"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e70f1ff7281810327b354ecaeba6cdce7fe498483338207fe7edfae1b21c212"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fef0706a5df7ba5f301a6920b28b0a4013ac06623aed96a6180e95c110b82a"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-win32.whl", hash = "sha256:07524370bdce055d4f106b7fed1afdfc86facd7d004cbb71adeaff3e06861bf6"},
|
||||
{file = "jsonschema_rs-0.29.1-cp311-cp311-win_amd64.whl", hash = "sha256:36fa23c85333baa8ce5bf0564fb19de3d95b7640c0cab9e3205ddc44a62fdbf0"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9fe7529faa6a84d23e31b1f45853631e4d4d991c85f3d50e6d1df857bb52b72d"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5d7e385298f250ed5ce4928fd59fabf2b238f8167f2c73b9414af8143dfd12e"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64a29be0504731a2e3164f66f609b9999aa66a2df3179ecbfc8ead88e0524388"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e91defda5dfa87306543ee9b34d97553d9422c134998c0b64855b381f8b531d"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f87680a6a1c16000c851d3578534ae3c154da894026c2a09a50f727bd623d4"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-win32.whl", hash = "sha256:bcfc0d52ecca6c1b2fbeede65c1ad1545de633045d42ad0c6699039f28b5fb71"},
|
||||
{file = "jsonschema_rs-0.29.1-cp312-cp312-win_amd64.whl", hash = "sha256:a414c162d687ee19171e2d8aae821f396d2f84a966fd5c5c757bd47df0954452"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0afee5f31a940dec350a33549ec03f2d1eda2da3049a15cd951a266a57ef97ee"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c38453a5718bcf2ad1b0163d128814c12829c45f958f9407c69009d8b94a1232"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5dc8bdb1067bf4f6d2f80001a636202dc2cea027b8579f1658ce8e736b06557f"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bcfe23992623a540169d0845ea8678209aa2fe7179941dc7c512efc0c2b6b46"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f2a526c0deacd588864d3400a0997421dffef6fe1df5cfda4513a453c01ad42"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-win32.whl", hash = "sha256:68acaefb54f921243552d15cfee3734d222125584243ca438de4444c5654a8a3"},
|
||||
{file = "jsonschema_rs-0.29.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c4e5a61ac760a2fc3856a129cc84aa6f8fba7b9bc07b19fe4101050a8ecc33c"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b46c4769204ff3a5af62eecd5443bf3a65a4094af02da6cb284d8df054193c7c"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:a456a6567ddc24e8390e67c698d68f74737f3f7047fdce86a7b655c737852955"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b493b4837d0423f9b4bb455f6f6ff001529fa522216e347addfa0517644895d"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ad422c7971cdb535408a130be767dd176e231ca756e42bc16098d6357bbfc5"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0808b3d78034a256ffdd10b6ffd67e8115b9258c692b972f353ee6ed4a4ff3c6"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-win32.whl", hash = "sha256:0a561d3b87075cd347a5a605799d60194aea4d923de6d4082ca854d2444ea8d7"},
|
||||
{file = "jsonschema_rs-0.29.1-cp38-cp38-win_amd64.whl", hash = "sha256:6cd5023eca31479473e87f39be87a4e31f5c879cfc403f610e985e18244c4c31"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d72c3b2a24936fde3f9cb3befec470e5ea23cf844f098f30f57d683630771cdf"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6142c8041f73a2dd67d7540f0609bd95e101f3d893d04471338e2508488319f4"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e27b0a8114643cacd6dda7796d40555b393605ca21cc0384505b9ffda4106d12"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b888c984640245f99dfd19397cb4723cd8c1781295427af50c995c49c616d561"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e7ea704031aeff7fed671c7c71c01118710d6ebe0144114afaa944789a88f1"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-win32.whl", hash = "sha256:ce58eef1742c8dadbf50318085d77ccbe81e30f4bf05ce42eb710403641c6cf2"},
|
||||
{file = "jsonschema_rs-0.29.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1a2b3f1c756579fc82b7d29def521e9532d6739b527ef446208ba5dd7e516e9"},
|
||||
{file = "jsonschema_rs-0.29.1.tar.gz", hash = "sha256:a9f896a9e4517630374f175364705836c22f09d5bd5bbb06ec0611332b6702fd"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
bench = ["fastjsonschema (>=2.20.0)", "jsonschema (>=4.23.0)", "pytest-benchmark (>=4.0.0)"]
|
||||
tests = ["flask (>=2.2.5)", "hypothesis (>=6.79.4)", "pytest (>=7.4.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2024.10.1"
|
||||
@@ -1364,6 +1540,39 @@ PyYAML = ">=5.3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
|
||||
typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.1.23"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.11"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "langgraph_api-0.1.23-py3-none-any.whl", hash = "sha256:61a4ce49f12348118a19005412761d57940b10364d55d79562eac79eec5cea79"},
|
||||
{file = "langgraph_api-0.1.23.tar.gz", hash = "sha256:e978b3c8ef0f0f4808c35525dac4a55ff2de898ad533457a872b5464eb7e6fd6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cloudpickle = ">=3.0.0,<4.0.0"
|
||||
cryptography = ">=42.0.0,<45.0"
|
||||
httpx = ">=0.25.0"
|
||||
jsonschema-rs = ">=0.20.0,<0.30"
|
||||
langchain-core = {version = ">=0.2.38", markers = "python_version < \"4.0\""}
|
||||
langgraph = {version = ">=0.2.56", markers = "python_version < \"4.0\""}
|
||||
langgraph-checkpoint = {version = ">=2.0.23", markers = "python_version < \"4.0\""}
|
||||
langgraph-runtime-inmem = ">=0.0.7"
|
||||
langgraph-sdk = {version = ">=0.1.63,<0.2.0", markers = "python_version < \"4.0\""}
|
||||
langsmith = ">=0.1.63"
|
||||
orjson = ">=3.9.7"
|
||||
pyjwt = ">=2.9.0,<3.0.0"
|
||||
sse-starlette = ">=2.1.0,<2.2.0"
|
||||
starlette = ">=0.38.6"
|
||||
structlog = ">=24.1.0,<26"
|
||||
tenacity = ">=8.0.0"
|
||||
uvicorn = ">=0.26.0"
|
||||
watchfiles = ">=0.13"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.25"
|
||||
@@ -1420,6 +1629,27 @@ langgraph-checkpoint = "^2.0.15"
|
||||
type = "directory"
|
||||
url = "../checkpoint-sqlite"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-cli"
|
||||
version = "0.2.8"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "langgraph_cli-0.2.8-py3-none-any.whl", hash = "sha256:b0f28bdafba6c98154c84f2aa439bb6653ef4fcb5e941dcd591ffe1662277473"},
|
||||
{file = "langgraph_cli-0.2.8.tar.gz", hash = "sha256:9091aa12bf826572446cb5564604a8a6f750c9dcaa0cd9fb1067128a53ac2282"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.1.7,<9.0.0"
|
||||
langgraph-api = {version = ">=0.1.20", optional = true, markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""}
|
||||
langgraph-runtime-inmem = {version = ">=0.0.8", optional = true, markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""}
|
||||
python-dotenv = {version = ">=0.8.0", optional = true, markers = "extra == \"inmem\""}
|
||||
|
||||
[package.extras]
|
||||
inmem = ["langgraph-api (>=0.1.20) ; python_version >= \"3.11\" and python_version < \"4.0\"", "langgraph-runtime-inmem (>=0.0.8) ; python_version >= \"3.11\" and python_version < \"4.0\"", "python-dotenv (>=0.8.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.1.8"
|
||||
@@ -1438,6 +1668,27 @@ langgraph-checkpoint = "^2.0.10"
|
||||
type = "directory"
|
||||
url = "../prebuilt"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-runtime-inmem"
|
||||
version = "0.0.9"
|
||||
description = "Inmem implementation for the LangGraph API server."
|
||||
optional = false
|
||||
python-versions = ">=3.11.0"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "langgraph_runtime_inmem-0.0.9-py3-none-any.whl", hash = "sha256:8d71df0b3b70142012346f8a309df84bbc4baadaeb9c045d95fd363681bbf08f"},
|
||||
{file = "langgraph_runtime_inmem-0.0.9.tar.gz", hash = "sha256:dac377ba0228e0fdc406ad505db1de6ace12c1e3908e07b08508db78cdcd58e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blockbuster = ">=1.5.24,<2.0.0"
|
||||
langgraph = {version = ">=0.2", markers = "python_version < \"4.0\""}
|
||||
langgraph-checkpoint = {version = ">=2.0.25", markers = "python_version < \"4.0\""}
|
||||
sse-starlette = ">=2"
|
||||
starlette = ">=0.37"
|
||||
structlog = ">23"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.63"
|
||||
@@ -2273,6 +2524,7 @@ files = [
|
||||
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
||||
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
||||
]
|
||||
markers = {dev = "python_version < \"4.0\""}
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.6.0"
|
||||
@@ -2384,6 +2636,7 @@ files = [
|
||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
|
||||
{file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
|
||||
]
|
||||
markers = {dev = "python_version < \"4.0\""}
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
@@ -2403,6 +2656,25 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.1"
|
||||
description = "JSON Web Token implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
|
||||
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.4.0)"]
|
||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyperf"
|
||||
version = "2.8.0"
|
||||
@@ -3122,6 +3394,27 @@ files = [
|
||||
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
description = "SSE plugin for Starlette"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"},
|
||||
{file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
starlette = "*"
|
||||
uvicorn = "*"
|
||||
|
||||
[package.extras]
|
||||
examples = ["fastapi"]
|
||||
|
||||
[[package]]
|
||||
name = "stack-data"
|
||||
version = "0.6.3"
|
||||
@@ -3142,6 +3435,44 @@ pure-eval = "*"
|
||||
[package.extras]
|
||||
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"},
|
||||
{file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.6.2,<5"
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "structlog"
|
||||
version = "25.3.0"
|
||||
description = "Structured Logging for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "structlog-25.3.0-py3-none-any.whl", hash = "sha256:a341f5524004c158498c3127eecded091eb67d3a611e7a3093deca30db06e172"},
|
||||
{file = "structlog-25.3.0.tar.gz", hash = "sha256:8dab497e6f6ca962abad0c283c46744185e0c9ba900db52a423cb6db99f7abeb"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"]
|
||||
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"]
|
||||
tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
|
||||
typing = ["mypy (>=1.4)", "rich", "twisted"]
|
||||
|
||||
[[package]]
|
||||
name = "syrupy"
|
||||
version = "4.7.2"
|
||||
@@ -3351,6 +3682,26 @@ h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"},
|
||||
{file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.0"
|
||||
h11 = ">=0.8"
|
||||
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.21.0b1"
|
||||
@@ -3447,6 +3798,91 @@ files = [
|
||||
[package.extras]
|
||||
watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.0.5"
|
||||
description = "Simple, modern and high performance file watching and code reload in python."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\" and python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"},
|
||||
{file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"},
|
||||
{file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"},
|
||||
{file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"},
|
||||
{file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"},
|
||||
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"},
|
||||
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"},
|
||||
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"},
|
||||
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"},
|
||||
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"},
|
||||
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"},
|
||||
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"},
|
||||
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"},
|
||||
{file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
@@ -3673,4 +4109,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "770dcaa5816fffb667b5e3639c0ee5add24df0a99bb9a89a6d8fb2c4a79fb185"
|
||||
content-hash = "0001cc74cd9b037ecf88f7c9eaa485f80d0da9030790f555b9ab9ad487359f04"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -40,6 +40,7 @@ pyperf = "^2.7.0"
|
||||
py-spy = "^0.3.14"
|
||||
types-requests = "^2.32.0.20240914"
|
||||
pycryptodome = "^3.21.0"
|
||||
langgraph-cli = {extras = ["inmem"], version = "^0.2.8"}
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [ "E", "F", "I", "TID251", "UP" ]
|
||||
|
||||
@@ -317,6 +317,128 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_get_graph_loop
|
||||
'''
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "__start__",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
],
|
||||
"name": "__start__"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "human",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"utils",
|
||||
"runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "human"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"utils",
|
||||
"runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "__end__"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "human"
|
||||
},
|
||||
{
|
||||
"source": "agent",
|
||||
"target": "human"
|
||||
},
|
||||
{
|
||||
"source": "human",
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"conditional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
# ---
|
||||
# name: test_get_graph_loop.1
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> human;
|
||||
agent --> human;
|
||||
human --> agent;
|
||||
agent -.-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_defer_node[memory-False]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> rewrite_query;
|
||||
analyzer_one -.-> qa;
|
||||
retriever_one --> analyzer_one;
|
||||
retriever_one --> qa;
|
||||
retriever_two --> qa;
|
||||
rewrite_query --> retriever_one;
|
||||
rewrite_query --> retriever_two;
|
||||
qa --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_defer_node[memory-True]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> rewrite_query;
|
||||
analyzer_one -.-> qa;
|
||||
retriever_one --> analyzer_one;
|
||||
retriever_one --> qa;
|
||||
retriever_two --> qa;
|
||||
rewrite_query --> retriever_one;
|
||||
rewrite_query --> retriever_two;
|
||||
qa --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_then_defer_node[memory-True]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> rewrite_query;
|
||||
analyzer_one --> qa;
|
||||
analyzer_one --> retriever_one;
|
||||
retriever_one -.-> qa;
|
||||
retriever_two --> qa;
|
||||
rewrite_query -.-> analyzer_one;
|
||||
rewrite_query -.-> qa;
|
||||
rewrite_query -.-> retriever_two;
|
||||
qa --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_in_one_fan_out_state_graph_waiting_edge[memory]
|
||||
'''
|
||||
graph TD;
|
||||
|
||||
+164
-361
@@ -1,32 +1,44 @@
|
||||
import sys
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from langchain_core import __version__ as core_version
|
||||
from packaging import version
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.cache.memory import InMemoryCache
|
||||
from langgraph.cache.sqlite import SqliteCache
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
from tests.conftest_checkpointer import (
|
||||
_checkpointer_memory,
|
||||
_checkpointer_postgres,
|
||||
_checkpointer_postgres_aio,
|
||||
_checkpointer_postgres_aio_pipe,
|
||||
_checkpointer_postgres_aio_pool,
|
||||
_checkpointer_postgres_aio_shallow,
|
||||
_checkpointer_postgres_pipe,
|
||||
_checkpointer_postgres_pool,
|
||||
_checkpointer_postgres_shallow,
|
||||
_checkpointer_sqlite,
|
||||
_checkpointer_sqlite_aes,
|
||||
_checkpointer_sqlite_aio,
|
||||
)
|
||||
from tests.conftest_store import (
|
||||
_store_memory,
|
||||
_store_postgres,
|
||||
_store_postgres_aio,
|
||||
_store_postgres_aio_pipe,
|
||||
_store_postgres_aio_pool,
|
||||
_store_postgres_pipe,
|
||||
_store_postgres_pool,
|
||||
)
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
# TODO: fix this once core is released
|
||||
IS_LANGCHAIN_CORE_030_OR_GREATER = version.parse(core_version) >= version.parse(
|
||||
"0.3.0.dev0"
|
||||
@@ -47,219 +59,54 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
return mocker.patch("uuid.uuid4", side_effect=side_effect)
|
||||
|
||||
|
||||
# checkpointer fixtures
|
||||
@pytest.fixture(params=[True, False])
|
||||
def checkpoint_during(request: pytest.FixtureRequest) -> bool:
|
||||
return request.param
|
||||
|
||||
|
||||
# --- start of deprecated fixtures ---
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_memory():
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
with _checkpointer_sqlite() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite_aes():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes(
|
||||
key=b"1234567890123456"
|
||||
)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
with _checkpointer_sqlite_aes() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
with _checkpointer_postgres() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_shallow():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
with _checkpointer_postgres_shallow() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
with _checkpointer_postgres_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_shallow():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
with _checkpointer_postgres_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -269,9 +116,8 @@ async def awith_checkpointer(
|
||||
if checkpointer_name is None:
|
||||
yield None
|
||||
elif checkpointer_name == "memory":
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
@@ -291,143 +137,54 @@ async def awith_checkpointer(
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
# --- end of deprecated fixtures ---
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup() # Run in its own transaction
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
@pytest.fixture(scope="function", params=["sqlite", "memory"])
|
||||
def cache(request: pytest.FixtureRequest) -> Iterator[BaseCache]:
|
||||
if request.param == "sqlite":
|
||||
yield SqliteCache(path=":memory:")
|
||||
elif request.param == "memory":
|
||||
yield InMemoryCache()
|
||||
else:
|
||||
raise ValueError(f"Unknown cache type: {request.param}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database,
|
||||
pool_config={"max_size": 10},
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup() # Run in its own transaction
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
|
||||
) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_in_memory():
|
||||
yield InMemoryStore()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=["in_memory", "postgres", "postgres_pipe", "postgres_pool"],
|
||||
)
|
||||
def sync_store(request: pytest.FixtureRequest) -> Iterator[BaseStore]:
|
||||
store_name = request.param
|
||||
if store_name is None:
|
||||
yield None
|
||||
elif store_name == "in_memory":
|
||||
yield InMemoryStore()
|
||||
with _store_memory() as store:
|
||||
yield store
|
||||
elif store_name == "postgres":
|
||||
with _store_postgres() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_pipe":
|
||||
with _store_postgres_pipe() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_pool":
|
||||
with _store_postgres_pool() as store:
|
||||
yield store
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown store {store_name}")
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=["in_memory", "postgres_aio", "postgres_aio_pipe", "postgres_aio_pool"],
|
||||
)
|
||||
async def async_store(request: pytest.FixtureRequest) -> AsyncIterator[BaseStore]:
|
||||
store_name = request.param
|
||||
if store_name is None:
|
||||
yield None
|
||||
elif store_name == "in_memory":
|
||||
with _store_memory() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_aio":
|
||||
async with _store_postgres_aio() as store:
|
||||
yield store
|
||||
@@ -441,44 +198,90 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
raise NotImplementedError(f"Unknown store {store_name}")
|
||||
|
||||
|
||||
SHALLOW_CHECKPOINTERS_SYNC = ["postgres_shallow"]
|
||||
REGULAR_CHECKPOINTERS_SYNC = [
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
"memory",
|
||||
"sqlite",
|
||||
"sqlite_aes",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
],
|
||||
)
|
||||
def sync_checkpointer(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Iterator[BaseCheckpointSaver]:
|
||||
checkpointer_name = request.param
|
||||
if checkpointer_name == "memory":
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite":
|
||||
with _checkpointer_sqlite() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite_aes":
|
||||
with _checkpointer_sqlite_aes() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres":
|
||||
with _checkpointer_postgres() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_pipe":
|
||||
with _checkpointer_postgres_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_pool":
|
||||
with _checkpointer_postgres_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
],
|
||||
)
|
||||
async def async_checkpointer(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> AsyncIterator[BaseCheckpointSaver]:
|
||||
checkpointer_name = request.param
|
||||
if checkpointer_name == "memory":
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio":
|
||||
async with _checkpointer_postgres_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pipe":
|
||||
async with _checkpointer_postgres_aio_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pool":
|
||||
async with _checkpointer_postgres_aio_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
"memory",
|
||||
"sqlite",
|
||||
"sqlite_aes",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
"sqlite_aes",
|
||||
"postgres_shallow",
|
||||
]
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
*REGULAR_CHECKPOINTERS_SYNC,
|
||||
*SHALLOW_CHECKPOINTERS_SYNC,
|
||||
]
|
||||
SHALLOW_CHECKPOINTERS_ASYNC = ["postgres_aio_shallow"]
|
||||
REGULAR_CHECKPOINTERS_ASYNC = [
|
||||
ALL_CHECKPOINTERS_ASYNC = [
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
]
|
||||
ALL_CHECKPOINTERS_ASYNC = [
|
||||
*REGULAR_CHECKPOINTERS_ASYNC,
|
||||
*SHALLOW_CHECKPOINTERS_ASYNC,
|
||||
]
|
||||
ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [
|
||||
*ALL_CHECKPOINTERS_ASYNC,
|
||||
None,
|
||||
]
|
||||
ALL_STORES_SYNC = [
|
||||
"in_memory",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
]
|
||||
ALL_STORES_ASYNC = [
|
||||
"in_memory",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
"postgres_aio_shallow",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_memory():
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_sqlite_aes():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes(
|
||||
key=b"1234567890123456"
|
||||
)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres_shallow():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_shallow():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_checkpointer_memory",
|
||||
"_checkpointer_sqlite",
|
||||
"_checkpointer_sqlite_aes",
|
||||
"_checkpointer_postgres",
|
||||
"_checkpointer_postgres_shallow",
|
||||
"_checkpointer_postgres_pipe",
|
||||
"_checkpointer_postgres_pool",
|
||||
"_checkpointer_sqlite_aio",
|
||||
"_checkpointer_postgres_aio",
|
||||
"_checkpointer_postgres_aio_shallow",
|
||||
"_checkpointer_postgres_aio_pipe",
|
||||
"_checkpointer_postgres_aio_pool",
|
||||
]
|
||||
@@ -0,0 +1,154 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_memory():
|
||||
store = InMemoryStore()
|
||||
yield store
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup() # Run in its own transaction
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
|
||||
) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup() # Run in its own transaction
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database,
|
||||
pool_config={"max_size": 10},
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_store_memory",
|
||||
"_store_postgres",
|
||||
"_store_postgres_pipe",
|
||||
"_store_postgres_pool",
|
||||
"_store_postgres_aio",
|
||||
"_store_postgres_aio_pipe",
|
||||
"_store_postgres_aio_pool",
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
from typing import Annotated
|
||||
|
||||
from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
|
||||
from langchain_core.tools import tool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.graph.message import add_messages
|
||||
from tests.fake_chat import FakeChatModel
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[list[BaseMessage], add_messages]
|
||||
|
||||
|
||||
@tool
|
||||
def search_api(query: str) -> str:
|
||||
"""Searches the API for the query."""
|
||||
return f"result for {query}"
|
||||
|
||||
|
||||
tools = [search_api]
|
||||
tools_by_name = {t.name: t for t in tools}
|
||||
|
||||
|
||||
def get_model():
|
||||
model = FakeChatModel(
|
||||
messages=[
|
||||
AIMessage(
|
||||
id="ai1",
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
},
|
||||
],
|
||||
),
|
||||
AIMessage(
|
||||
id="ai2",
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call234",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another", "idx": 0},
|
||||
},
|
||||
{
|
||||
"id": "tool_call567",
|
||||
"name": "search_api",
|
||||
"args": {"query": "a third one", "idx": 1},
|
||||
},
|
||||
],
|
||||
),
|
||||
AIMessage(id="ai3", content="answer"),
|
||||
]
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
@task
|
||||
def foo():
|
||||
return "foo"
|
||||
|
||||
|
||||
@entrypoint()
|
||||
async def app(state: AgentState) -> AgentState:
|
||||
model = get_model()
|
||||
max_steps = 100
|
||||
messages = state["messages"][:]
|
||||
await foo() # Very useful call here ya know.
|
||||
for _ in range(max_steps):
|
||||
message = await model.ainvoke(messages)
|
||||
messages.append(message)
|
||||
if not message.tool_calls:
|
||||
break
|
||||
# Assume it's the search tool
|
||||
tool_results = await search_api.abatch(
|
||||
[t["args"]["query"] for t in message.tool_calls]
|
||||
)
|
||||
messages.extend(
|
||||
[
|
||||
ToolMessage(content=tool_res, tool_call_id=tc["id"])
|
||||
for tc, tool_res in zip(message.tool_calls, tool_results)
|
||||
]
|
||||
)
|
||||
|
||||
return entrypoint.final(value=messages[-1], save={"messages": messages})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json",
|
||||
"graphs": {
|
||||
"app": "tests/example_app/example_graph.py:app"
|
||||
},
|
||||
"dependencies": ["tests/example_app"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
langchain-core
|
||||
-e .
|
||||
@@ -17,11 +17,6 @@ from langgraph.types import Command, Interrupt, PregelTask, StateSnapshot, inter
|
||||
from langgraph.utils.config import patch_configurable
|
||||
from tests.any_int import AnyInt
|
||||
from tests.any_str import AnyDict, AnyObject, AnyStr
|
||||
from tests.conftest import (
|
||||
REGULAR_CHECKPOINTERS_ASYNC,
|
||||
REGULAR_CHECKPOINTERS_SYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
@@ -1593,16 +1588,11 @@ def test_migrate_checkpoints(source: str, target: str) -> None:
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
def test_latest_checkpoint_state_graph(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=checkpointer)
|
||||
app = builder.compile(checkpointer=sync_checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert [*app.stream({"query": "what is weather in sf"}, config)] == [
|
||||
@@ -1638,61 +1628,55 @@ def test_latest_checkpoint_state_graph(
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_latest_checkpoint_state_graph_async(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
async def test_latest_checkpoint_state_graph_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=async_checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert [
|
||||
c async for c in app.astream({"query": "what is weather in sf"}, config)
|
||||
] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
||||
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
||||
{
|
||||
"__interrupt__": (
|
||||
Interrupt(
|
||||
value="",
|
||||
resumable=True,
|
||||
ns=[AnyStr("qa:")],
|
||||
),
|
||||
)
|
||||
},
|
||||
]
|
||||
assert [
|
||||
c async for c in app.astream({"query": "what is weather in sf"}, config)
|
||||
] == [
|
||||
{"rewrite_query": {"query": "query: what is weather in sf"}},
|
||||
{"analyzer_one": {"query": "analyzed: query: what is weather in sf"}},
|
||||
{"retriever_two": {"docs": ["doc3", "doc4"]}},
|
||||
{"retriever_one": {"docs": ["doc1", "doc2"]}},
|
||||
{
|
||||
"__interrupt__": (
|
||||
Interrupt(
|
||||
value="",
|
||||
resumable=True,
|
||||
ns=[AnyStr("qa:")],
|
||||
),
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
assert [c async for c in app.astream(Command(resume=""), config)] == [
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
assert [c async for c in app.astream(Command(resume=""), config)] == [
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
|
||||
# check history with current checkpoints matches expected history
|
||||
history = [c async for c in app.aget_state_history(config)]
|
||||
expected_history = get_expected_history()
|
||||
assert len(history) == len(expected_history)
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[1]
|
||||
assert history[2] == expected_history[2]
|
||||
assert history[3] == expected_history[3]
|
||||
assert history[4] == expected_history[4]
|
||||
assert history[5] == expected_history[5]
|
||||
# check history with current checkpoints matches expected history
|
||||
history = [c async for c in app.aget_state_history(config)]
|
||||
expected_history = get_expected_history()
|
||||
assert len(history) == len(expected_history)
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[1]
|
||||
assert history[2] == expected_history[2]
|
||||
assert history[3] == expected_history[3]
|
||||
assert history[4] == expected_history[4]
|
||||
assert history[5] == expected_history[5]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_version", ["3", "2-start:*", "2-quadratic"])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
def test_saved_checkpoint_state_graph(
|
||||
request: pytest.FixtureRequest,
|
||||
checkpointer_name: str,
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
checkpoint_version: str,
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=checkpointer)
|
||||
app = builder.compile(checkpointer=sync_checkpointer)
|
||||
|
||||
thread1 = "1"
|
||||
config = {"configurable": {"thread_id": thread1, "checkpoint_ns": ""}}
|
||||
@@ -1704,8 +1688,8 @@ def test_saved_checkpoint_state_graph(
|
||||
for write in checkpoint.pending_writes:
|
||||
grouped_writes[write[0]].append(write[1:])
|
||||
for tid, group in grouped_writes.items():
|
||||
checkpointer.put_writes(checkpoint.config, group, tid)
|
||||
checkpointer.put(
|
||||
sync_checkpointer.put_writes(checkpoint.config, group, tid)
|
||||
sync_checkpointer.put(
|
||||
patch_configurable(config, {"checkpoint_id": parent_id}),
|
||||
checkpoint.checkpoint,
|
||||
checkpoint.metadata,
|
||||
@@ -1753,71 +1737,65 @@ def test_saved_checkpoint_state_graph(
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_version", ["3", "2-start:*", "2-quadratic"])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_saved_checkpoint_state_graph_async(
|
||||
checkpointer_name: str,
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
checkpoint_version: str,
|
||||
) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=checkpointer)
|
||||
builder = make_state_graph()
|
||||
app = builder.compile(checkpointer=async_checkpointer)
|
||||
|
||||
thread1 = "1"
|
||||
config = {"configurable": {"thread_id": thread1, "checkpoint_ns": ""}}
|
||||
thread1 = "1"
|
||||
config = {"configurable": {"thread_id": thread1, "checkpoint_ns": ""}}
|
||||
|
||||
# save checkpoints
|
||||
parent_id: Optional[str] = None
|
||||
for checkpoint in reversed(SAVED_CHECKPOINTS[checkpoint_version]):
|
||||
grouped_writes = defaultdict(list)
|
||||
for write in checkpoint.pending_writes:
|
||||
grouped_writes[write[0]].append(write[1:])
|
||||
for tid, group in grouped_writes.items():
|
||||
await checkpointer.aput_writes(checkpoint.config, group, tid)
|
||||
await checkpointer.aput(
|
||||
patch_configurable(config, {"checkpoint_id": parent_id}),
|
||||
checkpoint.checkpoint,
|
||||
checkpoint.metadata,
|
||||
checkpoint.checkpoint["channel_versions"],
|
||||
)
|
||||
parent_id = checkpoint.checkpoint["id"]
|
||||
|
||||
# load history
|
||||
history = [c async for c in app.aget_state_history(config)]
|
||||
# check history with saved checkpoints matches expected history
|
||||
exc_task_results: int = 0
|
||||
if checkpoint_version == "2-start:*":
|
||||
exc_task_results = 1
|
||||
elif checkpoint_version == "2-quadratic":
|
||||
exc_task_results = 2
|
||||
expected_history = get_expected_history(exc_task_results=exc_task_results)
|
||||
assert len(history) == len(expected_history)
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[1]
|
||||
assert history[2] == expected_history[2]
|
||||
assert history[3] == expected_history[3]
|
||||
assert history[4] == expected_history[4]
|
||||
assert history[5] == expected_history[5]
|
||||
|
||||
# resume from 2nd to latest checkpoint
|
||||
assert [
|
||||
c async for c in app.astream(Command(resume=""), history[1].config)
|
||||
] == [
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
# new checkpoint should match the latest checkpoint in history
|
||||
latest_state = await app.aget_state(config)
|
||||
assert (
|
||||
StateSnapshot(
|
||||
values=latest_state.values,
|
||||
next=latest_state.next,
|
||||
config=patch_configurable(
|
||||
latest_state.config, {"checkpoint_id": AnyStr()}
|
||||
),
|
||||
metadata=AnyDict(latest_state.metadata),
|
||||
created_at=AnyStr(),
|
||||
parent_config=latest_state.parent_config,
|
||||
tasks=latest_state.tasks,
|
||||
interrupts=latest_state.interrupts,
|
||||
)
|
||||
== history[0]
|
||||
# save checkpoints
|
||||
parent_id: Optional[str] = None
|
||||
for checkpoint in reversed(SAVED_CHECKPOINTS[checkpoint_version]):
|
||||
grouped_writes = defaultdict(list)
|
||||
for write in checkpoint.pending_writes:
|
||||
grouped_writes[write[0]].append(write[1:])
|
||||
for tid, group in grouped_writes.items():
|
||||
await async_checkpointer.aput_writes(checkpoint.config, group, tid)
|
||||
await async_checkpointer.aput(
|
||||
patch_configurable(config, {"checkpoint_id": parent_id}),
|
||||
checkpoint.checkpoint,
|
||||
checkpoint.metadata,
|
||||
checkpoint.checkpoint["channel_versions"],
|
||||
)
|
||||
parent_id = checkpoint.checkpoint["id"]
|
||||
|
||||
# load history
|
||||
history = [c async for c in app.aget_state_history(config)]
|
||||
# check history with saved checkpoints matches expected history
|
||||
exc_task_results: int = 0
|
||||
if checkpoint_version == "2-start:*":
|
||||
exc_task_results = 1
|
||||
elif checkpoint_version == "2-quadratic":
|
||||
exc_task_results = 2
|
||||
expected_history = get_expected_history(exc_task_results=exc_task_results)
|
||||
assert len(history) == len(expected_history)
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[1]
|
||||
assert history[2] == expected_history[2]
|
||||
assert history[3] == expected_history[3]
|
||||
assert history[4] == expected_history[4]
|
||||
assert history[5] == expected_history[5]
|
||||
|
||||
# resume from 2nd to latest checkpoint
|
||||
assert [c async for c in app.astream(Command(resume=""), history[1].config)] == [
|
||||
{"qa": {"answer": "doc1,doc2,doc3,doc4"}},
|
||||
]
|
||||
# new checkpoint should match the latest checkpoint in history
|
||||
latest_state = await app.aget_state(config)
|
||||
assert (
|
||||
StateSnapshot(
|
||||
values=latest_state.values,
|
||||
next=latest_state.next,
|
||||
config=patch_configurable(latest_state.config, {"checkpoint_id": AnyStr()}),
|
||||
metadata=AnyDict(latest_state.metadata),
|
||||
created_at=AnyStr(),
|
||||
parent_config=latest_state.parent_config,
|
||||
tasks=latest_state.tasks,
|
||||
interrupts=latest_state.interrupts,
|
||||
)
|
||||
== history[0]
|
||||
)
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import pytest
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from tests.conftest import (
|
||||
REGULAR_CHECKPOINTERS_ASYNC,
|
||||
REGULAR_CHECKPOINTERS_SYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
def test_interruption_without_state_updates(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool
|
||||
) -> None:
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
@@ -34,8 +28,7 @@ def test_interruption_without_state_updates(
|
||||
builder.add_edge("step_2", "step_3")
|
||||
builder.add_edge("step_3", END)
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
graph = builder.compile(checkpointer=sync_checkpointer, interrupt_after="*")
|
||||
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
@@ -56,10 +49,8 @@ def test_interruption_without_state_updates(
|
||||
assert n_checkpoints == (5 if checkpoint_during else 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_interruption_without_state_updates_async(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool
|
||||
) -> None:
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
@@ -79,23 +70,22 @@ async def test_interruption_without_state_updates_async(
|
||||
builder.add_edge("step_2", "step_3")
|
||||
builder.add_edge("step_3", END)
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
graph = builder.compile(checkpointer=async_checkpointer, interrupt_after="*")
|
||||
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ("step_2",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (3 if checkpoint_during else 1)
|
||||
await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ("step_2",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (3 if checkpoint_during else 1)
|
||||
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ("step_3",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (4 if checkpoint_during else 2)
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ("step_3",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (4 if checkpoint_during else 2)
|
||||
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ()
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (5 if checkpoint_during else 3)
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
assert (await graph.aget_state(thread)).next == ()
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (5 if checkpoint_during else 3)
|
||||
|
||||
@@ -39,12 +39,9 @@ from langgraph.types import (
|
||||
interrupt,
|
||||
)
|
||||
from tests.agents import AgentAction, AgentFinish
|
||||
from tests.any_int import AnyInt
|
||||
from tests.any_str import AnyDict, AnyStr, UnsortedSequence
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
REGULAR_CHECKPOINTERS_SYNC,
|
||||
SHOULD_CHECK_SNAPSHOTS,
|
||||
)
|
||||
from tests.conftest import ALL_CHECKPOINTERS_SYNC, SHOULD_CHECK_SNAPSHOTS
|
||||
from tests.fake_chat import FakeChatModel
|
||||
from tests.fake_tracer import FakeTracer
|
||||
from tests.messages import (
|
||||
@@ -313,18 +310,16 @@ def test_invoke_two_processes_in_out_interrupt(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
def test_fork_always_re_runs_nodes(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
sync_checkpointer: BaseCheckpointSaver, mocker: MockerFixture
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
add_one = mocker.Mock(side_effect=lambda _: 1)
|
||||
|
||||
builder = StateGraph(Annotated[int, operator.add])
|
||||
builder.add_node("add_one", add_one)
|
||||
builder.add_edge(START, "add_one")
|
||||
builder.add_conditional_edges("add_one", lambda cnt: "add_one" if cnt < 6 else END)
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
graph = builder.compile(checkpointer=sync_checkpointer)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
@@ -2480,13 +2475,15 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
]
|
||||
}
|
||||
|
||||
assert [
|
||||
events = [
|
||||
c
|
||||
for c in app.stream(
|
||||
{"messages": [HumanMessage(content="what is weather in sf")]},
|
||||
stream_mode="messages",
|
||||
)
|
||||
] == [
|
||||
]
|
||||
|
||||
assert events[:3] == [
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
@@ -2528,8 +2525,8 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 2,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": (PULL, "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
@@ -2578,6 +2575,9 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
"ls_model_type": "chat",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
assert events[3:5] == UnsortedSequence(
|
||||
(
|
||||
_AnyIdToolMessage(
|
||||
content="result for another",
|
||||
@@ -2587,8 +2587,8 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 4,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": (PULL, "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
@@ -2601,11 +2601,13 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 4,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": (PULL, "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
)
|
||||
assert events[5:] == [
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="answer",
|
||||
@@ -2636,12 +2638,17 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
|
||||
model.i = 0 # reset the model
|
||||
|
||||
assert (
|
||||
app.invoke(
|
||||
{"messages": [HumanMessage(content="what is weather in sf")]},
|
||||
stream_mode="updates",
|
||||
)[0]["agent"]["messages"]
|
||||
== [
|
||||
invoke_updates_events = app.invoke(
|
||||
{"messages": [HumanMessage(content="what is weather in sf")]},
|
||||
stream_mode="updates",
|
||||
)
|
||||
|
||||
stream_updates_events = [
|
||||
*app.stream({"messages": [HumanMessage(content="what is weather in sf")]})
|
||||
]
|
||||
|
||||
for output in (invoke_updates_events, stream_updates_events):
|
||||
assert output[:3] == [
|
||||
{
|
||||
"agent": {
|
||||
"messages": [
|
||||
@@ -2690,6 +2697,8 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
assert output[3:5] == UnsortedSequence(
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
@@ -2698,6 +2707,12 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
name="search_api",
|
||||
tool_call_id="tool_call234",
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for a third one",
|
||||
name="search_api",
|
||||
@@ -2706,79 +2721,10 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
]
|
||||
}
|
||||
},
|
||||
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}},
|
||||
][0]["agent"]["messages"]
|
||||
)
|
||||
|
||||
assert [
|
||||
*app.stream({"messages": [HumanMessage(content="what is weather in sf")]})
|
||||
] == [
|
||||
{
|
||||
"agent": {
|
||||
"messages": [
|
||||
_AnyIdAIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call123",
|
||||
"name": "search_api",
|
||||
"args": {"query": "query"},
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": {
|
||||
"messages": [
|
||||
_AnyIdAIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "tool_call234",
|
||||
"name": "search_api",
|
||||
"args": {"query": "another"},
|
||||
},
|
||||
{
|
||||
"id": "tool_call567",
|
||||
"name": "search_api",
|
||||
"args": {"query": "a third one"},
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call234",
|
||||
),
|
||||
_AnyIdToolMessage(
|
||||
content="result for a third one",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call567",
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}},
|
||||
]
|
||||
)
|
||||
assert output[5:] == [
|
||||
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@@ -7389,13 +7335,9 @@ def test_branch_then(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
def test_send_dedupe_on_resume(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
class InterruptOnce:
|
||||
ticks: int = 0
|
||||
|
||||
@@ -7446,7 +7388,7 @@ def test_send_dedupe_on_resume(
|
||||
builder.add_conditional_edges("1", send_for_fun)
|
||||
builder.add_conditional_edges("2", route_to_three)
|
||||
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
graph = builder.compile(checkpointer=sync_checkpointer)
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert graph.invoke(["0"], thread1, checkpoint_during=checkpoint_during) == {
|
||||
"__interrupt__": [
|
||||
@@ -7461,8 +7403,6 @@ def test_send_dedupe_on_resume(
|
||||
assert builder.nodes["flaky"].runnable.func.ticks == 1
|
||||
# check state
|
||||
state = graph.get_state(thread1)
|
||||
if "shallow" in checkpointer_name:
|
||||
pytest.xfail("TODO: shallow checkpointer reports wrong next set")
|
||||
assert state.next == ("flaky",)
|
||||
# check history
|
||||
history = [c for c in graph.get_state_history(thread1)]
|
||||
|
||||
@@ -25,6 +25,7 @@ from typing_extensions import TypedDict
|
||||
from langgraph.channels.context import Context
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.untracked_value import UntrackedValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, PULL, PUSH, START
|
||||
from langgraph.graph.graph import Graph
|
||||
from langgraph.graph.message import MessageGraph, add_messages
|
||||
@@ -35,10 +36,10 @@ from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.types import PregelTask, Send, StateSnapshot, StreamWriter
|
||||
from tests.any_str import AnyDict, AnyStr
|
||||
from tests.any_int import AnyInt
|
||||
from tests.any_str import AnyDict, AnyStr, UnsortedSequence
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
REGULAR_CHECKPOINTERS_ASYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
from tests.fake_chat import FakeChatModel
|
||||
@@ -326,9 +327,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_fork_always_re_runs_nodes(
|
||||
checkpointer_name: str, mocker: MockerFixture
|
||||
async_checkpointer: BaseCheckpointSaver, mocker: MockerFixture
|
||||
) -> None:
|
||||
add_one = mocker.Mock(side_effect=lambda _: 1)
|
||||
|
||||
@@ -336,208 +336,201 @@ async def test_fork_always_re_runs_nodes(
|
||||
builder.add_node("add_one", add_one)
|
||||
builder.add_edge(START, "add_one")
|
||||
builder.add_conditional_edges("add_one", lambda cnt: "add_one" if cnt < 6 else END)
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
graph = builder.compile(checkpointer=async_checkpointer)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# start execution, stop at inbox
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(1, thread1, stream_mode=["values", "updates"])
|
||||
] == [
|
||||
("values", 1),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 2),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 3),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 4),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 5),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 6),
|
||||
]
|
||||
# start execution, stop at inbox
|
||||
assert [
|
||||
c async for c in graph.astream(1, thread1, stream_mode=["values", "updates"])
|
||||
] == [
|
||||
("values", 1),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 2),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 3),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 4),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 5),
|
||||
("updates", {"add_one": 1}),
|
||||
("values", 6),
|
||||
]
|
||||
|
||||
# list history
|
||||
history = [c async for c in graph.aget_state_history(thread1)]
|
||||
assert history == [
|
||||
StateSnapshot(
|
||||
values=6,
|
||||
next=(),
|
||||
tasks=(),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 5,
|
||||
"writes": {"add_one": 1},
|
||||
# list history
|
||||
history = [c async for c in graph.aget_state_history(thread1)]
|
||||
assert history == [
|
||||
StateSnapshot(
|
||||
values=6,
|
||||
next=(),
|
||||
tasks=(),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[1].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=5,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 4,
|
||||
"writes": {"add_one": 1},
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 5,
|
||||
"writes": {"add_one": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[1].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=5,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[2].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=4,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
"writes": {"add_one": 1},
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 4,
|
||||
"writes": {"add_one": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[2].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=4,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[3].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=3,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 2,
|
||||
"writes": {"add_one": 1},
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
"writes": {"add_one": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[3].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=3,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[4].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=2,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"add_one": 1},
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 2,
|
||||
"writes": {"add_one": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[4].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=2,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[5].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=1,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
"writes": None,
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"add_one": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[5].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=1,
|
||||
tasks=(PregelTask(AnyStr(), "add_one", (PULL, "add_one"), result=1),),
|
||||
next=("add_one",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[6].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=0,
|
||||
tasks=(
|
||||
PregelTask(AnyStr(), "__start__", (PULL, "__start__"), result=1),
|
||||
),
|
||||
next=("__start__",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
"writes": {"__start__": 1},
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
"writes": None,
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=history[6].config,
|
||||
interrupts=(),
|
||||
),
|
||||
StateSnapshot(
|
||||
values=0,
|
||||
tasks=(PregelTask(AnyStr(), "__start__", (PULL, "__start__"), result=1),),
|
||||
next=("__start__",),
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=None,
|
||||
interrupts=(),
|
||||
),
|
||||
]
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": AnyStr(),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
"writes": {"__start__": 1},
|
||||
"thread_id": "1",
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=None,
|
||||
interrupts=(),
|
||||
),
|
||||
]
|
||||
|
||||
# forking from any previous checkpoint should re-run nodes
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(None, history[0].config, stream_mode="updates")
|
||||
] == []
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(None, history[1].config, stream_mode="updates")
|
||||
] == [
|
||||
{"add_one": 1},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(None, history[2].config, stream_mode="updates")
|
||||
] == [
|
||||
{"add_one": 1},
|
||||
{"add_one": 1},
|
||||
]
|
||||
# forking from any previous checkpoint should re-run nodes
|
||||
assert [
|
||||
c async for c in graph.astream(None, history[0].config, stream_mode="updates")
|
||||
] == []
|
||||
assert [
|
||||
c async for c in graph.astream(None, history[1].config, stream_mode="updates")
|
||||
] == [
|
||||
{"add_one": 1},
|
||||
]
|
||||
assert [
|
||||
c async for c in graph.astream(None, history[2].config, stream_mode="updates")
|
||||
] == [
|
||||
{"add_one": 1},
|
||||
{"add_one": 1},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
@@ -2297,13 +2290,15 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
]
|
||||
}
|
||||
|
||||
assert [
|
||||
events = [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"messages": [HumanMessage(content="what is weather in sf")]},
|
||||
stream_mode="messages",
|
||||
)
|
||||
] == [
|
||||
]
|
||||
|
||||
assert events[:3] == [
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
@@ -2329,7 +2324,7 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
"ls_provider": "fakechatmodel",
|
||||
@@ -2345,8 +2340,8 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 2,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": ("__pregel_pull", "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
@@ -2388,13 +2383,16 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
"langgraph_step": 3,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
"ls_provider": "fakechatmodel",
|
||||
"ls_model_type": "chat",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
assert events[3:5] == UnsortedSequence(
|
||||
(
|
||||
_AnyIdToolMessage(
|
||||
content="result for another",
|
||||
@@ -2404,8 +2402,8 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 4,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": ("__pregel_pull", "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
@@ -2418,11 +2416,13 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 4,
|
||||
"langgraph_node": "tools",
|
||||
"langgraph_triggers": ("branch:to:tools",),
|
||||
"langgraph_path": ("__pregel_pull", "tools"),
|
||||
"langgraph_triggers": (PUSH,),
|
||||
"langgraph_path": (PUSH, AnyInt(), False),
|
||||
"langgraph_checkpoint_ns": AnyStr("tools:"),
|
||||
},
|
||||
),
|
||||
)
|
||||
assert events[5:] == [
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="answer",
|
||||
@@ -2431,7 +2431,7 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
"langgraph_step": 5,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
"ls_provider": "fakechatmodel",
|
||||
@@ -2440,12 +2440,13 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
),
|
||||
]
|
||||
|
||||
assert [
|
||||
stream_updates_events = [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"messages": [HumanMessage(content="what is weather in sf")]}
|
||||
)
|
||||
] == [
|
||||
]
|
||||
assert stream_updates_events[:3] == [
|
||||
{
|
||||
"agent": {
|
||||
"messages": [
|
||||
@@ -2494,6 +2495,8 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
assert stream_updates_events[3:5] == UnsortedSequence(
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
@@ -2502,6 +2505,12 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
name="search_api",
|
||||
tool_call_id="tool_call234",
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
_AnyIdToolMessage(
|
||||
content="result for a third one",
|
||||
name="search_api",
|
||||
@@ -2510,7 +2519,9 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
]
|
||||
}
|
||||
},
|
||||
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}},
|
||||
)
|
||||
assert stream_updates_events[5:] == [
|
||||
{"agent": {"messages": [_AnyIdAIMessage(content="answer")]}}
|
||||
]
|
||||
|
||||
|
||||
|
||||
+804
-108
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,30 @@
|
||||
import re
|
||||
import sys
|
||||
from typing import Annotated, Union
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AnyMessage, BaseMessage
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.graph import (
|
||||
Edge as DrawableEdge,
|
||||
)
|
||||
from langchain_core.runnables.graph import (
|
||||
Node as DrawableNode,
|
||||
)
|
||||
from langchain_core.runnables.graph import Edge as DrawableEdge
|
||||
from langchain_core.runnables.graph import Node as DrawableNode
|
||||
from langgraph_sdk.schema import StreamPart
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.graph import StateGraph, add_messages
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
from langgraph.pregel.types import StateSnapshot
|
||||
from langgraph.types import Interrupt
|
||||
from tests.example_app.example_graph import app
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
NEEDS_CONTEXTVARS = pytest.mark.skipif(
|
||||
sys.version_info < (3, 11),
|
||||
reason="Python 3.11+ is required for async contextvars support",
|
||||
)
|
||||
|
||||
|
||||
def test_with_config():
|
||||
@@ -421,6 +432,21 @@ def test_stream():
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(event="values", data={"chunk": "data3"}),
|
||||
StreamPart(event="updates", data={"chunk": "data4"}),
|
||||
StreamPart(
|
||||
event="messages",
|
||||
data=[
|
||||
{
|
||||
"content": [{"text": "Hello", "type": "text", "index": 0}],
|
||||
"type": "AIMessageChunk",
|
||||
},
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_llm",
|
||||
"langgraph_triggers": ["branch:to:call_llm"],
|
||||
"langgraph_path": ["__pregel_pull", "call_llm"],
|
||||
},
|
||||
],
|
||||
),
|
||||
StreamPart(
|
||||
event="updates",
|
||||
data={
|
||||
@@ -478,6 +504,30 @@ def test_stream():
|
||||
{"chunk": "data3"},
|
||||
]
|
||||
|
||||
# stream_mode messages
|
||||
stream_parts = []
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode="messages",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
(
|
||||
{
|
||||
"content": [{"text": "Hello", "type": "text", "index": 0}],
|
||||
"type": "AIMessageChunk",
|
||||
},
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_llm",
|
||||
"langgraph_triggers": ["branch:to:call_llm"],
|
||||
"langgraph_path": ["__pregel_pull", "call_llm"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
mock_sync_client.runs.stream.return_value = [
|
||||
StreamPart(event="updates", data={"chunk": "data3"}),
|
||||
StreamPart(event="updates", data={"chunk": "data4"}),
|
||||
@@ -555,6 +605,21 @@ async def test_astream():
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(event="values", data={"chunk": "data3"}),
|
||||
StreamPart(event="updates", data={"chunk": "data4"}),
|
||||
StreamPart(
|
||||
event="messages",
|
||||
data=[
|
||||
{
|
||||
"content": [{"text": "Hello", "type": "text", "index": 0}],
|
||||
"type": "AIMessageChunk",
|
||||
},
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_llm",
|
||||
"langgraph_triggers": ["branch:to:call_llm"],
|
||||
"langgraph_path": ["__pregel_pull", "call_llm"],
|
||||
},
|
||||
],
|
||||
),
|
||||
StreamPart(
|
||||
event="updates",
|
||||
data={
|
||||
@@ -613,6 +678,30 @@ async def test_astream():
|
||||
{"chunk": "data3"},
|
||||
]
|
||||
|
||||
# stream_mode messages
|
||||
stream_parts = []
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode="messages",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
(
|
||||
{
|
||||
"content": [{"text": "Hello", "type": "text", "index": 0}],
|
||||
"type": "AIMessageChunk",
|
||||
},
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_llm",
|
||||
"langgraph_triggers": ["branch:to:call_llm"],
|
||||
"langgraph_path": ["__pregel_pull", "call_llm"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
async_iter = MagicMock()
|
||||
async_iter.__aiter__.return_value = [
|
||||
StreamPart(event="updates", data={"chunk": "data3"}),
|
||||
@@ -963,3 +1052,131 @@ def test_sanitize_config():
|
||||
assert sanitized["metadata"]["level1"]["level2"]["level3"]["dict"] == {
|
||||
"a": {"b": {"c": "d"}}
|
||||
}
|
||||
|
||||
|
||||
"""Test RemoteGraph against an actual server."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def remote_graph() -> RemoteGraph:
|
||||
return RemoteGraph("app", url="http://localhost:2024")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nested_remote_graph(remote_graph: RemoteGraph) -> Pregel:
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list[AnyMessage], add_messages]
|
||||
|
||||
return (
|
||||
StateGraph(State)
|
||||
.add_node("nested", remote_graph)
|
||||
.add_edge("__start__", "nested")
|
||||
.compile(name="nested_remote_graph")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def nested_graph() -> Pregel:
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list[AnyMessage], add_messages]
|
||||
|
||||
return (
|
||||
StateGraph(State)
|
||||
.add_node("nested", app)
|
||||
.add_edge("__start__", "nested")
|
||||
.compile(name="nested_graph")
|
||||
)
|
||||
|
||||
|
||||
def get_message_dict(msg: Union[BaseMessage, dict]):
|
||||
# just get the core stuff from within the message
|
||||
if isinstance(msg, dict):
|
||||
return {
|
||||
"content": msg.get("content"),
|
||||
"type": msg.get("type"),
|
||||
"name": msg.get("name"),
|
||||
"tool_calls": msg.get("tool_calls"),
|
||||
"invalid_tool_calls": msg.get("invalid_tool_calls"),
|
||||
}
|
||||
return {
|
||||
"content": msg.content,
|
||||
"type": msg.type,
|
||||
"name": msg.name,
|
||||
"tool_calls": getattr(msg, "tool_calls", None),
|
||||
"invalid_tool_calls": getattr(msg, "invalid_tool_calls", None),
|
||||
}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_remote_graph_basic_invoke(remote_graph: RemoteGraph) -> None:
|
||||
# Basic smoke test of the remote graph
|
||||
response = await remote_graph.ainvoke(
|
||||
{"messages": [{"role": "user", "content": "hello"}]}
|
||||
)
|
||||
assert response == {
|
||||
"content": "answer",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"type": "ai",
|
||||
"name": None,
|
||||
"id": "ai3",
|
||||
"example": False,
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": None,
|
||||
}
|
||||
|
||||
|
||||
class monotonic_uid:
|
||||
def __init__(self):
|
||||
self._uid = 0
|
||||
|
||||
def __call__(self, match=None):
|
||||
val = self._uid
|
||||
self._uid += 1
|
||||
hexval = f"{val:032x}"
|
||||
uuid_str = f"{hexval[:8]}-{hexval[8:12]}-{hexval[12:16]}-{hexval[16:20]}-{hexval[20:32]}"
|
||||
return uuid_str
|
||||
|
||||
|
||||
uid_pattern = re.compile(
|
||||
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||
)
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_remote_graph_stream_messages_tuple(
|
||||
nested_graph: Pregel, nested_remote_graph: Pregel
|
||||
) -> None:
|
||||
events = []
|
||||
namespaces = []
|
||||
uid_generator = monotonic_uid()
|
||||
async for ns, messages in nested_remote_graph.astream(
|
||||
{"messages": [{"role": "user", "content": "hello"}]},
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
):
|
||||
events.extend(messages)
|
||||
namespaces.append(
|
||||
tuple(uid_pattern.sub(uid_generator, ns_part) for ns_part in ns)
|
||||
)
|
||||
inmem_events = []
|
||||
inmem_namespaces = []
|
||||
uid_generator = monotonic_uid()
|
||||
async for ns, messages in nested_graph.astream(
|
||||
{"messages": [{"role": "user", "content": "hello"}]},
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
):
|
||||
inmem_events.extend(messages)
|
||||
inmem_namespaces.append(
|
||||
tuple(uid_pattern.sub(uid_generator, ns_part) for ns_part in ns)
|
||||
)
|
||||
assert len(events) == len(inmem_events)
|
||||
assert len(namespaces) == len(inmem_namespaces)
|
||||
|
||||
coerced_events = [get_message_dict(e) for e in events]
|
||||
coerced_inmem_events = [get_message_dict(e) for e in inmem_events]
|
||||
assert coerced_events == coerced_inmem_events
|
||||
# TODO: Fix the namespace matching in the next api release.
|
||||
# assert namespaces == inmem_namespaces
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import functools
|
||||
import inspect
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -141,27 +140,6 @@ def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
|
||||
return prompt_runnable
|
||||
|
||||
|
||||
def _convert_modifier_to_prompt(func: F) -> F:
|
||||
"""Decorator that converts state_modifier kwarg to prompt kwarg."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
prompt = kwargs.get("prompt")
|
||||
state_modifier = kwargs.pop("state_modifier", None)
|
||||
if sum(p is not None for p in (prompt, state_modifier)) > 1:
|
||||
raise ValueError(
|
||||
"Expected only one of (prompt, state_modifier), got multiple values"
|
||||
)
|
||||
|
||||
if state_modifier is not None:
|
||||
prompt = state_modifier
|
||||
|
||||
kwargs["prompt"] = prompt
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return cast(F, wrapper)
|
||||
|
||||
|
||||
def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> bool:
|
||||
if isinstance(model, RunnableSequence):
|
||||
model = next(
|
||||
@@ -260,7 +238,6 @@ def _validate_chat_history(
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
@_convert_modifier_to_prompt
|
||||
def create_react_agent(
|
||||
model: Union[str, LanguageModelLike],
|
||||
tools: Union[Sequence[Union[BaseTool, Callable]], ToolNode],
|
||||
@@ -277,7 +254,7 @@ def create_react_agent(
|
||||
interrupt_before: Optional[list[str]] = None,
|
||||
interrupt_after: Optional[list[str]] = None,
|
||||
debug: bool = False,
|
||||
version: Literal["v1", "v2"] = "v1",
|
||||
version: Literal["v1", "v2"] = "v2",
|
||||
name: Optional[str] = None,
|
||||
) -> CompiledGraph:
|
||||
"""Creates an agent graph that calls tools in a loop until a stopping condition is met.
|
||||
@@ -701,6 +678,13 @@ def create_react_agent(
|
||||
break
|
||||
if m.name in should_return_direct:
|
||||
return END
|
||||
|
||||
# handle a case of parallel tool calls where
|
||||
# the tool w/ `return_direct` was executed in a different `Send`
|
||||
if isinstance(m, AIMessage) and m.tool_calls:
|
||||
if any(call["name"] in should_return_direct for call in m.tool_calls):
|
||||
return END
|
||||
|
||||
return entrypoint
|
||||
|
||||
if should_return_direct:
|
||||
|
||||
+117
-398
@@ -1,30 +1,36 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator, Optional
|
||||
from uuid import UUID, uuid4
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from langchain_core import __version__ as core_version
|
||||
from packaging import version
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
from tests.conftest_checkpointer import (
|
||||
_checkpointer_memory,
|
||||
_checkpointer_postgres,
|
||||
_checkpointer_postgres_aio,
|
||||
_checkpointer_postgres_aio_pipe,
|
||||
_checkpointer_postgres_aio_pool,
|
||||
_checkpointer_postgres_pipe,
|
||||
_checkpointer_postgres_pool,
|
||||
_checkpointer_sqlite,
|
||||
_checkpointer_sqlite_aio,
|
||||
)
|
||||
from tests.conftest_store import (
|
||||
_store_memory,
|
||||
_store_postgres,
|
||||
_store_postgres_aio,
|
||||
_store_postgres_aio_pipe,
|
||||
_store_postgres_aio_pool,
|
||||
_store_postgres_pipe,
|
||||
_store_postgres_pool,
|
||||
)
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
# TODO: fix this once core is released
|
||||
IS_LANGCHAIN_CORE_030_OR_GREATER = version.parse(core_version) >= version.parse(
|
||||
"0.3.0.dev0"
|
||||
@@ -47,375 +53,41 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
# checkpointer fixtures
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_memory():
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_shallow():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_shallow():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_checkpointer(
|
||||
checkpointer_name: Optional[str],
|
||||
) -> AsyncIterator[BaseCheckpointSaver]:
|
||||
if checkpointer_name is None:
|
||||
yield None
|
||||
elif checkpointer_name == "memory":
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio":
|
||||
async with _checkpointer_postgres_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_shallow":
|
||||
async with _checkpointer_postgres_aio_shallow() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pipe":
|
||||
async with _checkpointer_postgres_aio_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pool":
|
||||
async with _checkpointer_postgres_aio_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup() # Run in its own transaction
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database,
|
||||
pool_config={"max_size": 10},
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup() # Run in its own transaction
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
|
||||
) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_in_memory():
|
||||
yield InMemoryStore()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=["in_memory", "postgres", "postgres_pipe", "postgres_pool"],
|
||||
)
|
||||
def sync_store(request: pytest.FixtureRequest) -> Iterator[BaseStore]:
|
||||
store_name = request.param
|
||||
if store_name is None:
|
||||
yield None
|
||||
elif store_name == "in_memory":
|
||||
yield InMemoryStore()
|
||||
with _store_memory() as store:
|
||||
yield store
|
||||
elif store_name == "postgres":
|
||||
with _store_postgres() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_pipe":
|
||||
with _store_postgres_pipe() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_pool":
|
||||
with _store_postgres_pool() as store:
|
||||
yield store
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown store {store_name}")
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=["in_memory", "postgres_aio", "postgres_aio_pipe", "postgres_aio_pool"],
|
||||
)
|
||||
async def async_store(request: pytest.FixtureRequest) -> AsyncIterator[BaseStore]:
|
||||
store_name = request.param
|
||||
if store_name is None:
|
||||
yield None
|
||||
elif store_name == "in_memory":
|
||||
with _store_memory() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_aio":
|
||||
async with _store_postgres_aio() as store:
|
||||
yield store
|
||||
@@ -429,20 +101,67 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
raise NotImplementedError(f"Unknown store {store_name}")
|
||||
|
||||
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
"memory",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
"postgres_shallow",
|
||||
]
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
"memory",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
],
|
||||
)
|
||||
def sync_checkpointer(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Iterator[BaseCheckpointSaver]:
|
||||
checkpointer_name = request.param
|
||||
if checkpointer_name == "memory":
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite":
|
||||
with _checkpointer_sqlite() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres":
|
||||
with _checkpointer_postgres() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_pipe":
|
||||
with _checkpointer_postgres_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_pool":
|
||||
with _checkpointer_postgres_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
ALL_CHECKPOINTERS_ASYNC = [
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
"postgres_aio_shallow",
|
||||
]
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
],
|
||||
)
|
||||
async def async_checkpointer(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> AsyncIterator[BaseCheckpointSaver]:
|
||||
checkpointer_name = request.param
|
||||
if checkpointer_name == "memory":
|
||||
with _checkpointer_memory() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio":
|
||||
async with _checkpointer_postgres_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pipe":
|
||||
async with _checkpointer_postgres_aio_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pool":
|
||||
async with _checkpointer_postgres_aio_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_memory():
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_checkpointer_memory",
|
||||
"_checkpointer_sqlite",
|
||||
"_checkpointer_postgres",
|
||||
"_checkpointer_postgres_pipe",
|
||||
"_checkpointer_postgres_pool",
|
||||
"_checkpointer_sqlite_aio",
|
||||
"_checkpointer_postgres_aio",
|
||||
"_checkpointer_postgres_aio_pipe",
|
||||
"_checkpointer_postgres_aio_pool",
|
||||
]
|
||||
@@ -0,0 +1,154 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_memory():
|
||||
store = InMemoryStore()
|
||||
yield store
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup() # Run in its own transaction
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _store_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
|
||||
) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup() # Run in its own transaction
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database,
|
||||
pool_config={"max_size": 10},
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_store_memory",
|
||||
"_store_postgres",
|
||||
"_store_postgres_pipe",
|
||||
"_store_postgres_pool",
|
||||
"_store_postgres_aio",
|
||||
"_store_postgres_aio_pipe",
|
||||
"_store_postgres_aio_pool",
|
||||
]
|
||||
@@ -55,12 +55,7 @@ from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.types import Command, Interrupt, interrupt
|
||||
from langgraph.utils.config import get_stream_writer
|
||||
from tests.any_str import AnyStr
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
IS_LANGCHAIN_CORE_030_OR_GREATER,
|
||||
awith_checkpointer,
|
||||
)
|
||||
from tests.conftest import IS_LANGCHAIN_CORE_030_OR_GREATER
|
||||
from tests.messages import _AnyIdHumanMessage, _AnyIdToolMessage
|
||||
from tests.model import FakeToolCallingModel
|
||||
|
||||
@@ -69,20 +64,14 @@ pytestmark = pytest.mark.anyio
|
||||
REACT_TOOL_CALL_VERSIONS = ["v1", "v2"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
def test_no_prompt(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, version: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
def test_no_prompt(sync_checkpointer: BaseCheckpointSaver, version: str) -> None:
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
[],
|
||||
checkpointer=checkpointer,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
@@ -91,89 +80,72 @@ def test_no_prompt(
|
||||
expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
if checkpointer:
|
||||
saved = checkpointer.get_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
saved = sync_checkpointer.get_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_no_prompt_async(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
agent = create_react_agent(model, [], checkpointer=checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
response = await agent.ainvoke({"messages": inputs}, thread, debug=True)
|
||||
expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
if checkpointer:
|
||||
saved = await checkpointer.aget_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
def test_passing_two_modifiers():
|
||||
async def test_no_prompt_async(async_checkpointer: BaseCheckpointSaver) -> None:
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
create_react_agent(model, [], state_modifier="Foo", prompt="Bar")
|
||||
agent = create_react_agent(model, [], checkpointer=async_checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
response = await agent.ainvoke({"messages": inputs}, thread, debug=True)
|
||||
expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
saved = await async_checkpointer.aget_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
def test_system_message_prompt():
|
||||
prompt = SystemMessage(content="Foo")
|
||||
for agent in (
|
||||
create_react_agent(FakeToolCallingModel(), [], prompt=prompt),
|
||||
create_react_agent(FakeToolCallingModel(), [], state_modifier=prompt),
|
||||
):
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Foo-hi?", id="0", tool_calls=[])]
|
||||
}
|
||||
assert response == expected_response
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], prompt=prompt)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Foo-hi?", id="0", tool_calls=[])]
|
||||
}
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
def test_string_prompt():
|
||||
prompt = "Foo"
|
||||
for agent in (
|
||||
create_react_agent(FakeToolCallingModel(), [], prompt=prompt),
|
||||
create_react_agent(FakeToolCallingModel(), [], state_modifier=prompt),
|
||||
):
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Foo-hi?", id="0", tool_calls=[])]
|
||||
}
|
||||
assert response == expected_response
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], prompt=prompt)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Foo-hi?", id="0", tool_calls=[])]
|
||||
}
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
def test_callable_prompt():
|
||||
@@ -181,16 +153,11 @@ def test_callable_prompt():
|
||||
modified_message = f"Bar {state['messages'][-1].content}"
|
||||
return [HumanMessage(content=modified_message)]
|
||||
|
||||
for agent in (
|
||||
create_react_agent(FakeToolCallingModel(), [], prompt=prompt),
|
||||
create_react_agent(FakeToolCallingModel(), [], state_modifier=prompt),
|
||||
):
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Bar hi?", id="0")]
|
||||
}
|
||||
assert response == expected_response
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], prompt=prompt)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {"messages": inputs + [AIMessage(content="Bar hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
async def test_callable_prompt_async():
|
||||
@@ -198,16 +165,11 @@ async def test_callable_prompt_async():
|
||||
modified_message = f"Bar {state['messages'][-1].content}"
|
||||
return [HumanMessage(content=modified_message)]
|
||||
|
||||
for agent in (
|
||||
create_react_agent(FakeToolCallingModel(), [], prompt=prompt),
|
||||
create_react_agent(FakeToolCallingModel(), [], state_modifier=prompt),
|
||||
):
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = await agent.ainvoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Bar hi?", id="0")]
|
||||
}
|
||||
assert response == expected_response
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], prompt=prompt)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = await agent.ainvoke({"messages": inputs})
|
||||
expected_response = {"messages": inputs + [AIMessage(content="Bar hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
def test_runnable_prompt():
|
||||
@@ -215,16 +177,11 @@ def test_runnable_prompt():
|
||||
lambda state: [HumanMessage(content=f"Baz {state['messages'][-1].content}")]
|
||||
)
|
||||
|
||||
for agent in (
|
||||
create_react_agent(FakeToolCallingModel(), [], prompt=prompt),
|
||||
create_react_agent(FakeToolCallingModel(), [], state_modifier=prompt),
|
||||
):
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {
|
||||
"messages": inputs + [AIMessage(content="Baz hi?", id="0")]
|
||||
}
|
||||
assert response == expected_response
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], prompt=prompt)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
response = agent.invoke({"messages": inputs})
|
||||
expected_response = {"messages": inputs + [AIMessage(content="Baz hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@@ -543,19 +500,13 @@ class CustomStatePydantic(AgentStatePydantic):
|
||||
not IS_LANGCHAIN_CORE_030_OR_GREATER,
|
||||
reason="Langchain core 0.3.0 or greater is required",
|
||||
)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize("state_schema", [CustomState, CustomStatePydantic])
|
||||
def test_react_agent_update_state(
|
||||
request: pytest.FixtureRequest,
|
||||
checkpointer_name: str,
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
version: str,
|
||||
state_schema: StateSchemaType,
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
|
||||
@dec_tool
|
||||
def get_user_name(tool_call_id: Annotated[str, InjectedToolCallId]):
|
||||
"""Retrieve user name"""
|
||||
@@ -597,7 +548,7 @@ def test_react_agent_update_state(
|
||||
[get_user_name],
|
||||
state_schema=state_schema,
|
||||
prompt=prompt,
|
||||
checkpointer=checkpointer,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
@@ -618,21 +569,10 @@ def test_react_agent_update_state(
|
||||
not IS_LANGCHAIN_CORE_030_OR_GREATER,
|
||||
reason="Langchain core 0.3.0 or greater is required",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
[
|
||||
checkpointer
|
||||
for checkpointer in ALL_CHECKPOINTERS_SYNC
|
||||
if "shallow" not in checkpointer
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
def test_react_agent_parallel_tool_calls(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, version: str
|
||||
sync_checkpointer: BaseCheckpointSaver, version: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
human_assistance_execution_count = 0
|
||||
|
||||
@dec_tool
|
||||
@@ -663,7 +603,7 @@ def test_react_agent_parallel_tool_calls(
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
[human_assistance, get_weather],
|
||||
checkpointer=checkpointer,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
@@ -1078,7 +1018,9 @@ async def test_return_direct(version: str) -> None:
|
||||
),
|
||||
]
|
||||
model = FakeToolCallingModel(tool_calls=[second_tool_call, []])
|
||||
agent = create_react_agent(model, [tool_return_direct, tool_normal])
|
||||
agent = create_react_agent(
|
||||
model, [tool_return_direct, tool_normal], version=version
|
||||
)
|
||||
result = agent.invoke(
|
||||
{"messages": [HumanMessage(content="Test normal", id="hum1")]}
|
||||
)
|
||||
@@ -1107,7 +1049,9 @@ async def test_return_direct(version: str) -> None:
|
||||
),
|
||||
]
|
||||
model = FakeToolCallingModel(tool_calls=[both_tool_calls, []])
|
||||
agent = create_react_agent(model, [tool_return_direct, tool_normal])
|
||||
agent = create_react_agent(
|
||||
model, [tool_return_direct, tool_normal], version=version
|
||||
)
|
||||
result = agent.invoke({"messages": [HumanMessage(content="Test both", id="hum2")]})
|
||||
assert result["messages"] == [
|
||||
HumanMessage(content="Test both", id="hum2"),
|
||||
@@ -1149,14 +1093,9 @@ def test_inspect_react() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_react_with_subgraph_tools(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, version: str
|
||||
sync_checkpointer: BaseCheckpointSaver, version: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
@@ -1207,7 +1146,7 @@ def test_react_with_subgraph_tools(
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
tool_node,
|
||||
checkpointer=checkpointer,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
)
|
||||
result = agent.invoke(
|
||||
@@ -1294,14 +1233,9 @@ def test_tool_node_stream_writer() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_tool_node_node_interrupt(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, version: str
|
||||
sync_checkpointer: BaseCheckpointSaver, version: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
|
||||
def tool_normal(some_val: int) -> str:
|
||||
"""Tool docstring."""
|
||||
return "normal"
|
||||
@@ -1325,7 +1259,7 @@ def test_tool_node_node_interrupt(
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
[tool_interrupt, tool_normal],
|
||||
checkpointer=checkpointer,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
)
|
||||
result = agent.invoke({"messages": [HumanMessage("hi?")]}, config)
|
||||
@@ -1357,10 +1291,6 @@ def test_tool_node_node_interrupt(
|
||||
elif version == "v2":
|
||||
assert result["messages"] == expected_messages
|
||||
|
||||
# TODO: figure out why this is not working w/ shallow postgres checkpointer
|
||||
if "shallow" in checkpointer_name:
|
||||
return
|
||||
|
||||
state = agent.get_state(config)
|
||||
assert state.next == ("tools",)
|
||||
task = state.tasks[0]
|
||||
|
||||
@@ -151,6 +151,7 @@ class AsyncKafkaOrchestrator(AbstractAsyncContextManager):
|
||||
msg["input"],
|
||||
config=ensure_config(msg["config"]),
|
||||
stream=None,
|
||||
cache=self.graph.cache,
|
||||
store=self.graph.store,
|
||||
checkpointer=self.graph.checkpointer,
|
||||
nodes=graph.nodes,
|
||||
@@ -159,6 +160,7 @@ class AsyncKafkaOrchestrator(AbstractAsyncContextManager):
|
||||
stream_keys=graph.stream_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
trigger_to_nodes=graph.trigger_to_nodes,
|
||||
) as loop:
|
||||
if loop.tick(input_keys=graph.input_channels):
|
||||
# wait for checkpoint to be saved
|
||||
@@ -337,6 +339,7 @@ class KafkaOrchestrator(AbstractContextManager):
|
||||
msg["input"],
|
||||
config=ensure_config(msg["config"]),
|
||||
stream=None,
|
||||
cache=self.graph.cache,
|
||||
store=self.graph.store,
|
||||
checkpointer=self.graph.checkpointer,
|
||||
nodes=graph.nodes,
|
||||
@@ -345,6 +348,7 @@ class KafkaOrchestrator(AbstractContextManager):
|
||||
stream_keys=graph.stream_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
trigger_to_nodes=graph.trigger_to_nodes,
|
||||
) as loop:
|
||||
if loop.tick(input_keys=graph.input_channels):
|
||||
# wait for checkpoint to be saved
|
||||
|
||||
+471
-151
File diff suppressed because it is too large
Load Diff
@@ -16,10 +16,11 @@ from typing import (
|
||||
Json = Optional[dict[str, Any]]
|
||||
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
|
||||
|
||||
RunStatus = Literal["pending", "error", "success", "timeout", "interrupted"]
|
||||
RunStatus = Literal["pending", "running", "error", "success", "timeout", "interrupted"]
|
||||
"""
|
||||
Represents the status of a run:
|
||||
- "pending": The run is waiting to start.
|
||||
- "running": The run is currently executing.
|
||||
- "error": The run encountered an error and stopped.
|
||||
- "success": The run completed successfully.
|
||||
- "timeout": The run exceeded its time limit.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.66"
|
||||
version = "0.1.68"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user