Compare commits

...
96 Commits
Author SHA1 Message Date
Nuno CamposandGitHub 909a4591a8 Port checkpointer/store fixtures in langgraph-prebuilt to idiomatic pattern (#4629) 2025-05-09 19:42:44 +00:00
Nuno CamposandGitHub fc1ef29df7 Extend all tests using InMemorySaver to use all available checkpointers (#4627) 2025-05-09 12:34:59 -07:00
Nuno CamposandGitHub 30a72ced06 Implement update_state for functional api (#4626) 2025-05-09 12:34:45 -07:00
Nuno CamposandGitHub 302ae69e60 More idiomatic sync/async checkpointer fixtures in pytest (#4624) 2025-05-09 12:34:34 -07:00
Nuno CamposandGitHub 6777b5274c More idiomatic sync/async store fixtures in pytest (#4617)
- Use a single fixture that returns the store, instead of list of names
2025-05-09 12:34:16 -07:00
Nuno CamposandGitHub c5283cac09 Cache nodes/tasks (#4486)
- BaseCache interface defines the base class for cache storage adapters
- FileCache implements BaseCache with filesystem-backed storage
- Provide default cache key implementation which hashes args with pickle
- Update PregelExecutableTask with cache_key property for tasks that
opt-in to caching
- Update PregelLoop, PregelRunner to get/set from cache as appropriate

TODO
- [x]  Call match_cached_writes in async PregelRunner
- [ ]  Implement RedisCache to use in LGP
- [x]  Add more tests
2025-05-09 12:34:04 -07:00
Nuno Campos 562d64bbb7 Lint 2025-05-09 12:07:59 -07:00
Nuno Campos 898f266f72 Overload clear method to delete all when called without args 2025-05-09 12:05:26 -07:00
Nuno Campos 331d5b07ce Limit depth 2025-05-09 12:00:08 -07:00
William FHandGitHub 62069439fc feat: support --base-image in langgraph dockerfile command (#4628) 2025-05-09 18:35:33 +00:00
Nuno Campos 0d9b664360 Extend all tests using InMemorySaver to use all available checkpointers 2025-05-09 11:11:04 -07:00
Nuno Campos e8cea06e7e Implement update_state for functional api 2025-05-09 10:54:26 -07:00
Nuno Campos ed7f038a19 Fix 2025-05-09 10:54:12 -07:00
William FHandGitHub e507f0b991 feat: Customize timeouts in get_client (#4622) 2025-05-09 10:07:35 -07:00
Nuno Campos af961e279b More idiomatic sync/async checkpointer fixtures in pytest 2025-05-09 09:59:43 -07:00
Nuno Campos dd55e5097b Move to sep file 2025-05-09 09:01:25 -07:00
Nuno Campos 92cc3f0e0e More idiomatic sync/async store fixtures in pytest
- Use a single fixture that returns the store, instead of list of names
2025-05-09 08:38:31 -07:00
Eugene YurtsevandGitHub 4210188ade docs: remove giscus comments (#4616) 2025-05-09 11:22:43 -04:00
Nuno Campos 6fc1df9013 Lint 2025-05-08 17:12:22 -07:00
Nuno Campos 7850c8d799 Lint 2025-05-08 17:08:47 -07:00
Nuno Campos 83d2f93566 Lint 2025-05-08 16:57:21 -07:00
Nuno Campos c937d5f048 Lint 2025-05-08 16:54:06 -07:00
Nuno Campos 1a6395fd07 Move FileCache to sqlite package, add InMemoryCache 2025-05-08 16:50:36 -07:00
Nuno Campos 6e0041529e Add clear cache methods 2025-05-08 16:50:24 -07:00
Nuno Campos d5d6fc0fee Remove refresh 2025-05-08 16:50:24 -07:00
Nuno Campos 31b135f75d Add namespace to cache keys 2025-05-08 16:50:24 -07:00
Nuno Campos bebb0e8164 Lint 2025-05-08 16:50:24 -07:00
Nuno Campos 4761eb7696 Lint 2025-05-08 16:50:24 -07:00
Nuno Campos f1c1eaf229 Lint 2025-05-08 16:50:24 -07:00
Nuno Campos 0211886bf5 Lint 2025-05-08 16:50:24 -07:00
Nuno Campos 0dd9fba0af Lint 2025-05-08 16:50:24 -07:00
Nuno Campos f5bf77b3eb Lint 2025-05-08 16:50:09 -07:00
Nuno Campos 7c9f9aa89d Lint 2025-05-08 16:49:38 -07:00
Nuno Campos d38303494c Lint 2025-05-08 16:49:38 -07:00
Nuno Campos c4deb2c621 Finish implementation, add tests 2025-05-08 16:49:38 -07:00
Nuno Campos 42d88a769a Lint 2025-05-08 16:49:38 -07:00
Nuno Campos 14b07d06fa Re-implement using sqlite 2025-05-08 16:49:38 -07:00
Nuno Campos d04570f178 Lint 2025-05-08 16:49:38 -07:00
Nuno Campos 6c155f87c3 Lint 2025-05-08 16:49:38 -07:00
Nuno Campos 0264363083 Lint 2025-05-08 16:49:38 -07:00
Nuno Campos 1edf5cee89 Accept default cache_policy for graph/entrypoint/pregel 2025-05-08 16:49:38 -07:00
Nuno Campos 0e81699fec Lint 2025-05-08 16:49:01 -07:00
Nuno Campos a2a1a42c75 Fix 2025-05-08 16:49:01 -07:00
Nuno Campos 6b78bcd857 Implement ttl in FileCache 2025-05-08 16:49:01 -07:00
Nuno Campos d33c5a20e4 Fix 2025-05-08 16:49:01 -07:00
Nuno Campos 09fdc14d0a Output cached writes 2025-05-08 16:49:01 -07:00
Nuno Campos 64491a2b29 Lint 2025-05-08 16:49:01 -07:00
Nuno Campos 057da43cd0 Lint 2025-05-08 16:49:01 -07:00
Nuno Campos a11a62e68f Thread-safe delete 2025-05-08 16:49:01 -07:00
Nuno Campos 1d977f1c09 Rename 2025-05-08 16:49:01 -07:00
Nuno Campos 5cff35d1c3 Fix type annotation 2025-05-08 16:49:01 -07:00
Nuno Campos ed3f05260a Secure! 2025-05-08 16:49:01 -07:00
Nuno Campos b3ea406e81 Add pickle_fallback for json plus serializer 2025-05-08 16:49:01 -07:00
Nuno Campos 2a1c63ff9c Fixes 2025-05-08 16:49:01 -07:00
Nuno Campos 1aecde3cd8 Re-do with separate cache interface 2025-05-08 16:49:01 -07:00
Nuno Campos a446f34ed9 WIP 2025-05-08 16:48:46 -07:00
Nuno CamposandGitHub c2776449fd Deferred Node (#4269) 2025-05-08 23:03:57 +00:00
William FHandGitHub 88fd6b1e80 Start dev server externally (#4604)
Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2025-05-08 15:20:51 -07:00
William FHandGitHub 10b1676d0b Update python SDK docstrings (#4602) 2025-05-08 14:37:26 -07:00
William FHandGitHub c1bf678ed1 Update openapi spec (#4599)
Incorporate new arguments added to the API, including the `/mcp` paths
2025-05-08 09:14:18 -07:00
William FHandGitHub 9938b51d31 release: 0.4.3 (#4592) 2025-05-07 20:28:40 -07:00
b84ae660b8 langgraph: use tuples for streamed message events in RemoteGraph (#4589)
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-05-08 03:20:23 +00:00
William FHandGitHub e3146d8050 Fix remote streaming of subgraphs (#4590)
Because we were propagating the task ID config key, the stream mode was
always overridden as "values", meaning the token, etc. callback handlers
were never added within the remote graphs.

---------

Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2025-05-07 20:10:31 -07:00
Lauren Hirata SinghandGitHub d56c3b0f26 Update banner (#4588) 2025-05-07 20:12:35 -04:00
Lauren Hirata Singh cf89499507 Update banner 2025-05-07 20:07:31 -04:00
Vadym BardaandGitHub 79dffe20b2 docs: update multi agent multi-turn howto (#4582)
Fixes #4554
2025-05-07 16:06:28 -04:00
a9ea0cd28a Add a limit to Pregel.draw (#4575)
Co-authored-by: vbarda <vadym@langchain.dev>
2025-05-07 16:12:12 +00:00
GustafandGitHub ffaddab110 docs: Fixed incorrect tool in 'Write from tools' documentation (#4572)
I was having trouble following this section of the docs and I realized
it is because the incorrect tools is included in the tools list.
`update_user_info` was defined earlier in the file but never used.
2025-05-07 11:06:44 -04:00
William FHandGitHub 4aeaffef4e 0.4.2 (#4570)
Release
2025-05-06 18:28:37 -07:00
Lauren Hirata SinghandGitHub b44be763fb Fix typos (#4567) 2025-05-06 13:41:32 -07:00
langchain-infraandGitHub 00d2a1abd4 docs: update ingress requirement for self hosted control plane (#4564) 2025-05-06 14:51:37 -04:00
infra 2e8e9e4531 docs: update ingress requirement for self hosted control plane 2025-05-06 11:46:31 -07:00
William FHandGitHub 0e2471b401 Add LANGGRAPH_POSTGRES_POOL_MAX_SIZE env var (#4562) 2025-05-06 18:23:55 +00:00
Nuno CamposandGitHub e401ad7c75 langgraph: decouple name from assistant ID in RemoteGraph (#4550) 2025-05-05 18:22:54 -07:00
vbarda f1bfd6051a update 2025-05-05 21:02:10 -04:00
vbarda 998be75f34 langgraph: decouple name from assistant ID in RemoteGraph 2025-05-05 20:47:22 -04:00
Nuno CamposandGitHub e51442279a sdk-py: Prefix private functions in sdk with _ (#4542) 2025-05-05 15:19:24 -07:00
Nuno Campos 365dd5f459 sdk-py: Prefix private functions in sdk with _ 2025-05-05 14:43:34 -07:00
Jacob LeeandGitHub 8837f8452c docs: Fix JS example in custom auth how-tos (#4536)
Should be `defaultHeaders`:
https://github.com/langchain-ai/langgraph/blob/main/libs/sdk-js/src/client.ts#L76
2025-05-05 12:35:00 -07:00
Vadym BardaandGitHub 306915690c docs(concepts): update HITL Command section (#4533) 2025-05-05 18:44:32 +00:00
Vadym BardaandGitHub 0e61cc2cf6 prebuilt: remove state_modifier (#4439)
Removes support for `state_modifier` param in `create_react_agent`. To
specify system prompt / instructions use `prompt` parameter.
2025-05-05 09:49:28 -04:00
Sydney RunkleandGitHub a0a302dec5 prebuilt: switch to executing parallel tool calls via Send by default (#4438) 2025-05-05 09:49:09 -04:00
Vadym BardaandGitHub b19405b296 docs: hide old RAG tutorials (#4525) 2025-05-04 21:03:05 +00:00
Vadym BardaandGitHub 101ad12292 docs: update agentic rag tutorial (#4513) 2025-05-04 20:59:08 +00:00
Vadym BardaandGitHub df58a71567 docs: update api ref test (#4516) 2025-05-02 13:58:29 -04:00
Vadym BardaandGitHub ee04c32bb3 docs: update api ref links to be smaller / italicized (#4514) 2025-05-02 12:50:21 -04:00
William FHandGitHub 4e05db8537 Release checkpoint-sqlite (#4509) 2025-05-01 22:42:07 -07:00
William FHandGitHub d6e20e6d09 Add missing 'running' RunStatus (#4508) 2025-05-02 05:36:58 +00:00
Eugene YurtsevandGitHub dd4ad48864 docs: add /mcp endpoint concept for LangGraph Server (#4151)
Documents the /mcp endpoint for LangGraph Server
2025-05-01 22:14:22 -04:00
Vadym BardaandGitHub f79c8487d9 docs: remove nonexistent pages from nav (#4504) 2025-05-01 21:34:24 -04:00
Vadym BardaandGitHub 284e9a2cb4 docs: hide hierarchical tutorial from nav (#4503) 2025-05-01 21:24:12 -04:00
Vadym BardaandGitHub 9216e949e9 docs: update multi-agent supervisor tutorial to use handoffs (#4491) 2025-05-01 21:22:14 -04:00
gyudozaandGitHub a6e2d9e197 docs(rag): fix many typo of generate at RAG section (#4490)
While I was studying about RAG by using langgraph, I found these typos.
2025-05-02 01:19:24 +00:00
Eugene YurtsevandGitHub abae398a3a fix(docs): fix indent typo in code snippet in human-in-the-loop.md (#4501) 2025-05-01 21:18:15 -04:00
vbarda 78f809bc52 update other tests 2025-04-28 12:02:23 -04:00
vbarda 4b7ec256e9 prebuilt: switch to executing parallel tool calls via Send by default 2025-04-28 11:07:27 -04:00
99 changed files with 8734 additions and 3902 deletions
+1
View File
@@ -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)
+19
View File
@@ -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=
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
+4
View File
@@ -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"
+1 -1
View File
@@ -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).
+6 -16
View File
@@ -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?
+1
View File
@@ -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) |
|-------------------|-------------------|------------|
+188
View File
@@ -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.
+1 -1
View File
@@ -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
-7
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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 %}
-55
View File
@@ -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 %}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+48
View File
@@ -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."""
+73
View File
@@ -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_}")
+25 -3
View File
@@ -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
+4
View File
@@ -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
+165 -149
View File
@@ -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 -1
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
.langgraph_api/
.devserver.pid
+30 -4
View File
@@ -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
######################
+29 -17
View File
@@ -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
+6
View File
@@ -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")
+65 -21
View File
@@ -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,
)
+7
View File
@@ -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
+41 -7
View File
@@ -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:
+134 -23
View File
@@ -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."""
+168 -79
View File
@@ -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]}"
+41 -4
View File
@@ -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
+17 -3
View File
@@ -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
+118 -12
View File
@@ -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:
+9 -3
View File
@@ -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
+50 -25
View File
@@ -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:
+12 -4
View File
@@ -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
+79 -51
View File
@@ -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,
+26 -8
View File
@@ -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
+31
View File
@@ -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)
+440 -4
View File
@@ -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"
+2 -1
View File
@@ -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
View File
@@ -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",
]
+154
View File
@@ -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 .
+98 -120
View File
@@ -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]
)
+19 -29
View File
@@ -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)
+44 -104
View File
@@ -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)]
+220 -209
View File
@@ -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")]}}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+223 -6
View File
@@ -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
View File
@@ -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",
]
+154
View File
@@ -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",
]
+86 -156
View File
@@ -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
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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 -1
View File
@@ -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"