Compare commits

...
Author SHA1 Message Date
William FHandGitHub 781d0cf27a [CLI] Handle dependencies js up --watch 2024-12-16 16:27:56 -08:00
William Fu-Hinthorn 8a02ddd868 [CLI] Handle dependencies js up --watch 2024-12-16 16:19:23 -08:00
William FHandGitHub 4caa483478 [SDK] relax response typehint
So you can return a dict with extra args
2024-12-16 14:02:46 -08:00
William Fu-Hinthorn 006305f6a8 [SDK] relax response typehint 2024-12-16 11:28:21 -08:00
Vadym BardaandGitHub d9b7aaa5cc langgraph: relax constraints in ToolNode Command validation (#2778) 2024-12-16 14:00:45 -05:00
Vadym BardaandGitHub d2794eda0a langgraph: relax type annotation for Command.update (#2777)
Addresses #2758 , #2747
2024-12-16 14:00:23 -05:00
William FHandGitHub 9cbef9b542 [SDK] Add HTTPException type
To make it easier to raise exceptions with custom status codes in the imported file.
2024-12-16 09:01:15 -08:00
William Fu-Hinthorn 343dc2d37a [SDK] Add HTTPException type 2024-12-16 08:45:44 -08:00
David DuongandGitHub ca0ff1d334 Merge pull request #2774 from langchain-ai/dqbd/studio-datasets-navigation
docs: update studio index to add datasets
2024-12-16 20:14:46 +04:00
Tat Dat Duong f5663ffa49 docs: update studio index to add datasets 2024-12-16 16:44:43 +01:00
David DuongandGitHub e7477a9315 Merge pull request #2680 from langchain-ai/dqbd/add-to-dataset-docs
feat(studio): add Add to Dataset docs
2024-12-16 19:31:49 +04:00
Tat Dat Duong 18c083b60a Use S3 for assets 2024-12-16 16:24:05 +01:00
Vadym BardaandGitHub 2f0e3c66d1 docs: update structured output how-to guide (#2773)
Fixes #2760
2024-12-16 09:51:12 -05:00
ZapironandGitHub d9ec185e72 docs: Update correct ID for initial ToolNode hyperlink (#2767)
Automatically leads to the correct `ToolNode` section instead of the top
2024-12-16 09:15:34 -05:00
William FHandGitHub 87fba0ecd0 [CLI] Add openapi param to config
For langraph api
2024-12-14 07:33:07 -08:00
William FHandGitHub 30e6b5482f Merge branch 'main' into wfh/cli/add_auth_env_var 2024-12-14 07:26:34 -08:00
William Fu-Hinthorn 77b42e0867 [CLI] Add openapi param to config 2024-12-14 07:25:14 -08:00
William FHandGitHub cbe92e3e55 [CLI] Add auth param to langgraph.json
Preliminary for supporting custom auth.
2024-12-13 17:09:09 -08:00
William Fu-Hinthorn 70f2efc9a1 Update dev 2024-12-13 17:01:01 -08:00
William Fu-Hinthorn dbf5b4920e [CLI] Add auth env var 2024-12-13 16:36:35 -08:00
William FHandGitHub 9291ae8646 Merge pull request #2761 from langchain-ai/wfh/auth/types
[SDK] Add auth types
2024-12-13 16:25:41 -08:00
William Fu-Hinthorn 2713082707 [SDK] Add auth types 2024-12-13 16:17:35 -08:00
Eugene YurtsevandGitHub 89eb938b30 docs: fix typo in example
Fix typo in thread config
2024-12-13 12:47:30 -05:00
Eugene Yurtsev 26e97492b7 add missing thread config 2024-12-13 12:39:26 -05:00
Vadym BardaandGitHub 4c3958f0be docs: update custom tool call snippet (#2754) 2024-12-13 09:42:13 -05:00
980b592631 Fixed the code snippet in the Update State From Tools tutorial (#2752)
Hi,

While reading the [update state from
tools](https://langchain-ai.github.io/langgraph/how-tos/update-state-from-tools/)
tutorial. I noticed that this code snippet contains a syntax error:

```python
def call_tools(state):
    ...
    commands = [tools_by_name[call["name"].invoke(call, config={"coerce_tool_content": False}) for tool_call in tool_calls]
    return commands
```

There is a missing closing bracket `]` in the list comprehension.
Additionally, the variable `call` inside the list comprehension is
undefined, it should be `tool_call`.

Here is a corrected version of the code:

```python
def call_tools(state):
    ...
    commands = [tools_by_name[tool_call["name"]].invoke(tool_call, config={"coerce_tool_content": False}) for tool_call in tool_calls]
    return commands
```

---------

Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
2024-12-13 09:40:18 -05:00
Imad SaddikandGitHub e494869c72 Fixed the documentation for the persistence concept (#2750)
Hi,

I was reading the
[persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state)
concept in LangGraph and found a sentence with a missing verb, so I
fixed it.
2024-12-13 09:38:17 -05:00
da0aac7556 Updating MongoDB checkpointer docs (#2743)
This PR updates the [How-to
guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/)
on using the MongoDB checkpointer.

The guide currently explains how to create a custom MongoDB
checkpointer, but we now have a checkpointer implementation available
via the `langgraph-checkpoint-mongodb` library. This PR updates the
current resource to guide users on how to use this implementation.

---------

Co-authored-by: ajosh0504 <apoorva.joshi@mongodb.com>
Co-authored-by: vbarda <vadym@langchain.dev>
2024-12-12 15:31:57 -05:00
Vadym BardaandGitHub a200027cda checkpoint: release 2.0.9 (#2744) 2024-12-12 15:06:54 -05:00
Eugene YurtsevandGitHub 8cd57f7457 Merge pull request #2742 from langchain-ai/eugene/more_info_in_human_in_the_loop
concepts: HIL add more context to interrupt section
2024-12-12 14:55:00 -05:00
Nuno Campos 26c1f1ee7a sdk-py 0.1.44 2024-12-12 11:48:20 -08:00
Eugene Yurtsev 33af251a09 x 2024-12-12 14:01:33 -05:00
Eugene YurtsevandGitHub 17dc588c81 docs: concepts HIL add example w/ subgraph call
Add an example with subgraph call to illustrate the flow
2024-12-12 12:02:36 -05:00
Eugene Yurtsev 258060593b x 2024-12-12 11:54:04 -05:00
Vadym BardaandGitHub fbe513835f docs: update type annotations (#2739) 2024-12-12 11:53:55 -05:00
Eugene Yurtsev f319b1e107 x 2024-12-12 11:50:09 -05:00
Nuno CamposandGitHub cbb7348998 Merge pull request #2736 from langchain-ai/nc/12dec/sdk-command-keys
sdk-py: Strip out unused keys in command parameter
2024-12-12 08:18:19 -08:00
Nuno CamposandGitHub 2d8246e7c4 Merge pull request #2735 from langchain-ai/vb/relax-strict-keys
checkpoint: set strict_map_key=False in serde
2024-12-12 08:15:48 -08:00
Nuno Campos e5ea4f51c7 sdk-py: Strip out unused keys in command parameter 2024-12-12 08:10:43 -08:00
vbarda a031f8294e all lines 2024-12-12 11:06:45 -05:00
vbarda 7d940a4a96 lint 2024-12-12 10:58:50 -05:00
vbarda 6ae0c83c83 checkpoint: set strict_map_key=False in serde 2024-12-12 10:58:07 -05:00
Vadym BardaandGitHub 083a14c2c5 docs: update how-to to remove agent wrapper (#2721) 2024-12-12 08:15:17 -05:00
Nuno CamposandGitHub e4db5c2ca4 Merge pull request #2728 from langchain-ai/nc/11dec/more-tests
lib: Add more tests
2024-12-11 17:41:47 -08:00
Nuno Campos 1d2b50e438 Fix 2024-12-11 17:34:21 -08:00
Nuno Campos 5146c9fcdf Disable for old py 2024-12-11 17:04:58 -08:00
Nuno Campos e8a2f7ef92 lib: Add more tests
- add more unit tests (courtesy of claude)
- move tests with large assertions to separate file
2024-12-11 16:20:53 -08:00
Vadym BardaandGitHub 0400c5236e docs: update redis how-to (#2727)
Fixes #2712
2024-12-11 23:22:47 +00:00
Vadym BardaandGitHub 67f96063e2 docs: update min lib version for howto (#2726) 2024-12-11 17:11:57 -05:00
Vadym BardaandGitHub 44cdbc781f langgraph: release 0.2.59 (#2725) 2024-12-11 16:45:13 -05:00
Vadym BardaandGitHub f642fb6545 langgraph[fix]: pass config to tools (#2724)
Fixes #2723
2024-12-11 21:43:29 +00:00
Andrew NguonlyandGitHub ff3bc2f982 docs: Add details about Cloud SaaS deployment time (#2722) 2024-12-11 13:19:19 -08:00
William FHandGitHub e1925a8dcb Merge pull request #2720 from langchain-ai/wfh/docs/missing_backticks 2024-12-11 12:34:51 -08:00
William FHandGitHub fe83a151bb Merge branch 'main' into wfh/docs/missing_backticks 2024-12-11 12:34:38 -08:00
William Fu-Hinthorn d18c9449ec [docs] Add missing backicks 2024-12-11 12:33:33 -08:00
Eugene YurtsevandGitHub 12a15c3cb4 docs: document interrupt & HIL
- Document interrupt reference
- Update conceptual guides for HIL
- Split time-travel conceptual guide
- Split breakpoints into separate conceptual guide
- Update relevant how-tos
- Update how-to index page for HIL with more information and recommendations
- New how-to for multi turn conversation
2024-12-11 14:00:21 -05:00
Eugene Yurtsev 189358cb91 one more link fix 2024-12-11 13:22:17 -05:00
Eugene YurtsevandGitHub d16004c0b6 Merge branch 'main' into eugene/document_interrupt 2024-12-11 13:14:01 -05:00
Eugene Yurtsev fdf19a5be9 x 2024-12-11 13:12:29 -05:00
Eugene Yurtsev d24ce62c3f x 2024-12-11 13:10:42 -05:00
Eugene Yurtsev 3ffed8d38d x 2024-12-11 13:08:57 -05:00
vbarda 25d4512744 update cassettes 2024-12-11 13:03:27 -05:00
Eugene Yurtsev 630195a108 fix one more link 2024-12-11 12:50:32 -05:00
Eugene YurtsevandGitHub 475e16b8ed Merge pull request #2718 from langchain-ai/eugene/fix_links
fix links
2024-12-11 12:43:19 -05:00
Eugene Yurtsev 32702dea08 x 2024-12-11 12:42:54 -05:00
Eugene Yurtsev 3db266bb93 x 2024-12-11 12:41:03 -05:00
Eugene Yurtsev 11ce54d7e4 x 2024-12-11 12:39:53 -05:00
Eugene Yurtsev 54a5e45d21 x 2024-12-11 12:37:54 -05:00
vbarda 0ad470162c fix links 2024-12-11 11:54:19 -05:00
Eugene YurtsevandGitHub 37436c5cd6 Merge pull request #2717 from langchain-ai/eugene/breakpoints_take_one_hundred
re-org concepts
2024-12-11 11:42:15 -05:00
Eugene Yurtsev 7f48428d16 x 2024-12-11 11:41:00 -05:00
Eugene Yurtsev d9c0a5d827 x 2024-12-11 11:40:09 -05:00
Eugene Yurtsev 6907d1b775 x 2024-12-11 11:32:33 -05:00
Eugene YurtsevandGitHub bec3055561 Merge pull request #2714 from langchain-ai/eugene/add_more_hil_patterns
Beef up concept, remove how to
2024-12-11 11:09:05 -05:00
Eugene YurtsevandGitHub 2f535a803c Merge pull request #2716 from langchain-ai/eugene/document_command
concepts: document command as HIL
2024-12-11 11:08:57 -05:00
Eugene Yurtsev fbb11a6d2e x 2024-12-11 11:08:24 -05:00
Eugene YurtsevandGitHub 3f348e3268 Merge pull request #2715 from langchain-ai/eugene/fix_typo_123
fix typo
2024-12-11 11:05:14 -05:00
Eugene Yurtsev 6b80fa6718 x 2024-12-11 11:02:28 -05:00
Eugene Yurtsev 82fa597e84 x 2024-12-11 10:58:58 -05:00
Eugene Yurtsev f6c44ec154 x 2024-12-11 10:58:10 -05:00
vbarda a03900be7a minor fix 2024-12-11 09:56:20 -05:00
vbarda 7144f7db41 typos 2024-12-11 09:24:48 -05:00
Eugene YurtsevandGitHub 8f1db66c17 Merge pull request #2711 from langchain-ai/eugene/more_changes
more changes
2024-12-10 23:54:57 -05:00
Eugene Yurtsev c2556aa2fe x 2024-12-10 23:54:15 -05:00
Eugene YurtsevandGitHub d468655f62 Merge pull request #2710 from langchain-ai/eugene/more_concept_work
more concepts changes
2024-12-10 23:25:01 -05:00
Eugene Yurtsev 6153c777fb x 2024-12-10 23:24:26 -05:00
Eugene Yurtsev a4d49b4e77 x 2024-12-10 23:23:58 -05:00
Eugene Yurtsev 789c732866 x 2024-12-10 22:52:17 -05:00
Eugene YurtsevandGitHub 51f85ffa84 Merge pull request #2709 from langchain-ai/eugene/update_index_page
langgraph: update index page
2024-12-10 22:21:42 -05:00
Eugene Yurtsev 31dd6c65a9 x 2024-12-10 22:20:55 -05:00
Eugene YurtsevandGitHub cb225dde7f Merge pull request #2708 from langchain-ai/eugene/wait_for_user_input_improve
wait for user input improvements
2024-12-10 21:53:52 -05:00
Eugene Yurtsev 0cb530e588 x 2024-12-10 21:53:19 -05:00
Eugene YurtsevandGitHub f0f3e11b0e Merge pull request #2707 from langchain-ai/eugene/fix_typos
fix typo
2024-12-10 21:39:09 -05:00
Eugene YurtsevandGitHub 55fbff89f4 Merge pull request #2706 from langchain-ai/eugene/update_breakpoints_2
docs: update resume link
2024-12-10 21:38:43 -05:00
Eugene Yurtsev 327bb369d7 x 2024-12-10 21:38:20 -05:00
Eugene YurtsevandGitHub 30eb2d00e2 Merge pull request #2703 from langchain-ai/vb/update-wait-for-input
docs: update wait for user input how-to
2024-12-10 21:34:46 -05:00
Eugene Yurtsev 45d1033092 x 2024-12-10 21:32:31 -05:00
Eugene Yurtsev 5a0ae2157c update resume link 2024-12-10 21:22:00 -05:00
Eugene YurtsevandGitHub e001b7a35f Merge pull request #2702 from langchain-ai/eugene/update_more_docs
eugene/update more docs
2024-12-10 21:18:49 -05:00
Eugene YurtsevandGitHub a41b9bb83c Merge pull request #2704 from langchain-ai/eugene/update_glossary 2024-12-10 21:18:34 -05:00
vbarda 04f6a6ccd1 update 2024-12-10 21:13:38 -05:00
Andrew NguonlyandGitHub ce15790210 docs: Add section about Cloud SaaS autoscaling (#2705) 2024-12-10 16:58:25 -08:00
Eugene Yurtsev 04b76f55a0 x 2024-12-10 18:23:51 -05:00
vbarda f52a8728ff docs: update wait for user input how-to 2024-12-10 18:19:33 -05:00
Eugene Yurtsev 686ee31b75 x 2024-12-10 18:10:55 -05:00
Eugene Yurtsev 47dcb2d105 x 2024-12-10 18:10:14 -05:00
Eugene Yurtsev 3a611fb20d update dynamic breakpoints 2024-12-10 18:09:07 -05:00
Eugene Yurtsev baa88f3c97 x 2024-12-10 17:56:48 -05:00
Vadym BardaandGitHub 79aa88812d docs: update reivew tool calls how-to (#2700) 2024-12-10 17:38:48 -05:00
Eugene Yurtsev 81436ceef8 x 2024-12-10 17:22:22 -05:00
Nuno Campos 2b70dba0e0 0.2.58 2024-12-10 14:09:11 -08:00
Eugene Yurtsev e14a6cf98b Add multi turn conversation input 2024-12-10 17:06:08 -05:00
Nuno CamposandGitHub dc0398efd1 Merge pull request #2661 from langchain-ai/nc/5dec/perf
lib: Performance improvements
2024-12-10 14:04:02 -08:00
David DuongandGitHub 02f1904ba7 Merge pull request #2699 from langchain-ai/dqbd/sdk-js-0.0.32
feat(sdk-js): bump to 0.0.32
2024-12-11 02:00:21 +04:00
Nuno Campos 30f852e7b2 Fix 2024-12-10 13:56:26 -08:00
Tat Dat Duong 7fc6c4b1fa feat(sdk-js): bump to 0.0.32 2024-12-10 22:52:51 +01:00
Nuno Campos 7f8ec2c590 Fix 2024-12-10 13:46:45 -08:00
Vadym BardaandGitHub 611588613d docs: add an FAQ note for command vs cond edge (#2697) 2024-12-10 15:16:02 -05:00
Nuno Campos 11e80210a2 lib: Performance improvements
- don't create contextvars.Context/asyncio.Task in RunnableSeq (not needed as each step creates it if necessary)
- don't run in-memory-saver methods in background threads (no point as they hold the gil)
- avoid calling should_interrupt when no interrupts set
2024-12-10 11:40:24 -08:00
Nuno CamposandGitHub 60d742ea48 Merge pull request #2683 from langchain-ai/nc/9dec/invoke-command-goto
lib: Add support for invoke(Command(goto=<str>))
2024-12-10 11:39:10 -08:00
Nuno Campos a7ac9ffd4e Update test 2024-12-10 11:31:41 -08:00
Vadym BardaandGitHub 3d97b97c86 fix typo (#2696) 2024-12-10 14:19:23 -05:00
Nuno CamposandGitHub a7d1ecbb74 Merge pull request #2693 from langchain-ai/eugene/fix_test
langgraph[patch]: Fix unit test for Command(update)
2024-12-10 11:09:11 -08:00
vbarda 61f362f16e update dynamic breakpoints 2024-12-10 14:07:08 -05:00
Eugene YurtsevandNuno Campos 7cabc0a3dc reformat 2024-12-10 11:04:07 -08:00
Eugene YurtsevandNuno Campos f9cdfd3ac4 x 2024-12-10 11:03:56 -08:00
Eugene YurtsevandNuno Campos dd778f8ed6 qxqx 2024-12-10 11:03:56 -08:00
Nuno Campos df5d08f689 Fix 2024-12-10 11:03:01 -08:00
Nuno Campos a9b94f93ee Update again 2024-12-10 11:03:01 -08:00
Nuno Campos 5f869b9e75 Update test 2024-12-10 11:03:01 -08:00
Nuno Campos 79562f3f37 lib: Add support for invoke(Command(goto=<str>)) 2024-12-10 11:03:01 -08:00
Nuno Campos 081b2cbdcf Fix 2024-12-10 11:01:25 -08:00
Eugene YurtsevandNuno Campos 70eeb2a670 x 2024-12-10 11:00:58 -08:00
Nuno CamposandGitHub 0f287d986b Merge pull request #2695 from langchain-ai/nc/10dec/multistep-plan
lib: Add unit test for multistep planner graph
2024-12-10 10:54:37 -08:00
Nuno CamposandGitHub 1fd9da6718 Merge pull request #2691 from langchain-ai/dqbd/enhanced-config-type-extraction
fix(config): extract default values, description from pydantic models, typeddict and dataclass
2024-12-10 10:44:24 -08:00
Nuno Campos ef6c5b4711 lib: Add unit test for multistep planner graph 2024-12-10 10:40:49 -08:00
Eugene Yurtsev 77fe51fbe4 Merge branch 'main' into eugene/document_interrupt 2024-12-10 13:17:47 -05:00
Vadym BardaandGitHub 70a5ef6713 docs: small updates (#2694) 2024-12-10 12:02:24 -05:00
Tat Dat Duong 17c1a8db46 Fix lint 2024-12-10 17:42:01 +01:00
Vadym BardaandGitHub 97a51014c3 docs: add a how-to on updating state from tools (#2670) 2024-12-10 11:20:50 -05:00
Tat Dat Duong 5a30fc6a87 Handle PydanticUndefined, add tests 2024-12-10 17:06:37 +01:00
Tat Dat Duong 1f68bd0d83 Move to langgraph.utils.fields 2024-12-10 16:49:05 +01:00
vbarda fdfc5d9cda Revert "langgraph: release 0.2.58 (#2692)"
This reverts commit a9f5507006.
2024-12-10 10:35:01 -05:00
Vadym BardaandGitHub 1c3f65c931 docs: add tool use for Command concepts (#2669)
To be merged after #2656
2024-12-10 10:22:17 -05:00
Vadym BardaandGitHub a9f5507006 langgraph: release 0.2.58 (#2692) 2024-12-10 10:19:59 -05:00
Vadym BardaandGitHub 59bfa5d009 langgraph: allow tools to return Command in tool node (#2656) 2024-12-10 10:18:04 -05:00
Tat Dat Duong b4f11929f8 fix(config): extract default values, description from pydantic models, typeddict and dataclass 2024-12-10 15:24:56 +01:00
Vadym BardaandGitHub 038bec2e78 update callout (#2689) 2024-12-09 23:27:44 -05:00
Eugene Yurtsev 01cdb60b5d x 2024-12-09 23:14:19 -05:00
Eugene Yurtsev 5bfb9af5fe x 2024-12-09 22:53:35 -05:00
Eugene Yurtsev ac48612abb x 2024-12-09 22:51:06 -05:00
Eugene Yurtsev 73fb725f0c add pngs 2024-12-09 22:50:33 -05:00
Eugene Yurtsev b98a1337a5 x 2024-12-09 22:50:16 -05:00
Vadym BardaandGitHub c2a41039de docs: remove GraphCommand references (#2688) 2024-12-09 22:44:47 -05:00
33fe467d1f lib: Treat Command as "resuming" signal (#2682)
- so it works w interrupt_before/after

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2024-12-09 21:22:53 -05:00
Vadym BardaandGitHub 43b6c06f5c docs: update Command concept doc (#2686) 2024-12-09 21:19:16 -05:00
Vadym BardaandGitHub d81dec653d docs: temporarily fix link (#2685) 2024-12-09 21:09:32 -05:00
Vadym BardaandGitHub e1d8c6b113 docs: update multi-agent concept doc (#2684) 2024-12-09 21:01:14 -05:00
Vadym BardaandGitHub a64f9f80c0 docs: add a how to for multi-agent network (#2675) 2024-12-09 20:18:29 -05:00
Eugene Yurtsev a879de51f1 x 2024-12-09 18:13:39 -05:00
Eugene Yurtsev 60ab76c3e9 x 2024-12-09 18:12:17 -05:00
Eugene Yurtsev 09e9117674 x 2024-12-09 17:24:59 -05:00
Eugene Yurtsev acac19b95b x 2024-12-09 16:49:51 -05:00
Eugene Yurtsev 723bcfeaa2 x 2024-12-09 15:27:00 -05:00
Eugene Yurtsev c279421cbf x 2024-12-09 14:46:06 -05:00
Tat Dat Duong df1e48154a feat(studio): add Add to Dataset docs 2024-12-09 19:22:41 +01:00
Eugene Yurtsev d0bf7837bd x 2024-12-09 11:25:21 -05:00
Nuno CamposandGitHub a403e802fa Merge pull request #2679 from langchain-ai/nc/9dec/imperative-generator
lib: imperative api: Generators use yield to publish stream_mode=custom events
2024-12-09 08:22:31 -08:00
Nuno Campos e0a0958a60 lib: imperative api: Generators use yield to publish stream_mode=custom events 2024-12-09 08:14:41 -08:00
William FHandGitHub 3f1bdb9ebf Add sync support for the AsyncPostgresStore (#2673) 2024-12-09 07:12:52 -08:00
Nuno Campos b37c9d8a01 0.2.57 2024-12-07 11:49:10 -08:00
Nuno CamposandGitHub 1af1911aad Merge pull request #2378 from langchain-ai/nc/8nov/send-future
Imperative API
2024-12-07 11:48:45 -08:00
Eugene Yurtsev b4f7e06a1d x 2024-12-06 22:42:41 -05:00
Eugene Yurtsev 1dda28f8fb x 2024-12-06 22:31:28 -05:00
Eugene Yurtsev cc4718c5cb x 2024-12-06 22:31:18 -05:00
Eugene Yurtsev 0d580bdac7 x 2024-12-06 22:31:11 -05:00
Eugene Yurtsev a19d06e18c x 2024-12-06 17:00:18 -05:00
Eugene Yurtsev e16312da3f x 2024-12-06 16:52:33 -05:00
Nuno CamposandGitHub 6784a5a5b1 Merge pull request #2667 from langchain-ai/nc/6dec/support-mixed-list
lib: Support returning mixed list of commands and state updates
2024-12-06 08:31:03 -08:00
Nuno Campos 4e0e9a4eff Fix 2024-12-06 08:20:41 -08:00
Nuno Campos 015bf5e0a6 Add tests, missing return stmt 2024-12-06 08:17:50 -08:00
Nuno Campos 85fc26db43 lib: Support returning mixed list of commands and state updates 2024-12-06 08:03:26 -08:00
Vadym BardaandGitHub 5fa80e2a92 docs: update multi-agent tutorials to use Command (#2643) 2024-12-06 15:18:12 +00:00
Eugene Yurtsev 5e13460604 x 2024-12-05 23:31:54 -05:00
Eugene Yurtsev 750b97349e x 2024-12-05 21:32:10 -05:00
Eugene Yurtsev 6230c46830 x 2024-12-05 21:27:18 -05:00
Eugene Yurtsev 01b1080b6e x 2024-12-05 17:26:55 -05:00
Eugene Yurtsev 62ff2eb32d x 2024-12-05 17:16:34 -05:00
Eugene Yurtsev 4fd261765a x 2024-12-05 15:46:26 -05:00
Eugene Yurtsev 0a49f3003b x 2024-12-04 22:57:48 -05:00
Eugene Yurtsev e80098e297 x 2024-12-04 22:38:55 -05:00
Eugene Yurtsev 3d3647cd85 x 2024-12-04 22:36:43 -05:00
Eugene Yurtsev 291379dfb9 x 2024-12-04 22:32:11 -05:00
Eugene Yurtsev 9f93e48a67 x 2024-12-04 22:19:32 -05:00
Eugene Yurtsev de123d66a5 Merge branch 'main' into eugene/document_interrupt 2024-12-04 21:30:55 -05:00
Nuno Campos 2fa2469967 Update 2024-12-04 15:39:16 -08:00
Nuno Campos de86a46b3d Comment 2024-12-04 15:39:16 -08:00
Nuno Campos 9733db03c5 Wait until next tick to start send task 2024-12-04 15:39:16 -08:00
Nuno Campos e1f65012e6 Fix 2024-12-04 15:39:16 -08:00
Nuno Campos eb593d47dd Fix writes for task being saved against next checkpoint id 2024-12-04 15:39:16 -08:00
Nuno Campos 4e8f4ce440 Update 2024-12-04 15:39:16 -08:00
Nuno Campos 007d7e72b1 Add test for cancellation 2024-12-04 15:39:16 -08:00
Nuno Campos 2b77fdabee Lint 2024-12-04 15:39:16 -08:00
Nuno Campos 40d16593c7 Lint 2024-12-04 15:39:16 -08:00
Nuno Campos ec7bbe14b2 Lint 2024-12-04 15:39:16 -08:00
Nuno Campos 4c6323c585 Lint 2024-12-04 15:39:16 -08:00
Nuno Campos 2fe38f3940 Fix get_state 2024-12-04 15:39:16 -08:00
Nuno Campos 09ca964714 Wire up retry policy 2024-12-04 15:39:16 -08:00
Nuno Campos 0663d46c47 Rename 2024-12-04 15:39:16 -08:00
Nuno Campos a91dbf9b70 Lint 2024-12-04 15:38:43 -08:00
Nuno Campos d93be914c7 Fix stream order 2024-12-04 15:38:43 -08:00
Nuno Campos 287c29fbdc Fix async 2024-12-04 15:38:15 -08:00
Nuno Campos 90dd2b01b6 Comment 2024-12-04 15:38:15 -08:00
Nuno Campos a443b3b256 Fix 2024-12-04 15:38:15 -08:00
Nuno Campos 2e9aea6fc8 Lint 2024-12-04 15:38:15 -08:00
Nuno Campos 2895a69678 Lint 2024-12-04 15:38:15 -08:00
Nuno Campos 76a209835f Comments 2024-12-04 15:37:56 -08:00
Nuno Campos 872f54adf1 Get it working with interrupt (sync) 2024-12-04 15:37:56 -08:00
Nuno Campos 01a3c23a29 WIP 2024-12-04 15:37:56 -08:00
Nuno Campos 0461d45d76 Finish impl 2024-12-04 15:37:31 -08:00
Nuno Campos 7d8205633d Add call function to call a node and get a future
- Whereas Send is for fire-and-forget type of calls, new `call` and `acall` functions are for flows where you want to wait for the node to finish before doing something else
- Because we return regular python future objects (concurrent.futures.Future or asyncio.Future) all the python primitives for working with futures work, eg. wait, gather, etc
2024-12-04 15:37:31 -08:00
Eugene Yurtsev c75bfc1032 x 2024-12-04 17:22:09 -05:00
Eugene Yurtsev 6dc70b703d x 2024-12-04 17:21:47 -05:00
Eugene Yurtsev eb09909c22 x 2024-12-04 17:21:31 -05:00
110 changed files with 28862 additions and 19903 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
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
@@ -1 +0,0 @@
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
@@ -0,0 +1 @@
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
@@ -1 +0,0 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -1 +1 @@
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
@@ -1 +1 @@
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
@@ -1 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -0,0 +1 @@
eNqNVmtsFFUULqJREoMJz0R+MKxAUXd2Z7ZL220atfQFpS3QWdJCUza3M3d3hs7eO8y903ZtmkjFQJRIJkRjYsBAt7u6KX2EGjTlYYQYmsgf/khVIMbE+EMTjSQkRsA7s7t9F+if7t5zzne+c75zTrYv3QlNomG0ZFBDFJpApuwLsfvSJjxkQUKPpOKQqlhJ7t4lhfstU5vcpFJqkDK/HxiaDyCqmtjQZJ+M4/5O0R+HhIAYJMl2rCR+fOZijycOuiMUd0BEPGWcKASCXs6T92IvrT0eE+uQffJYBJoeZpUxo4Ko89SlAlpIOKpCrgsC9s/kNMSR6JueXi83HQkI0QhlZGaHM2wKu12gHYW6zqlQN7gEtjhZhXLHXFQJIK7GBEjWiIx9DhJNGC68C+ImnHrBWI8wvo4XAnH3LQcVIRCYsupYNGRYTvYej6zRhOMzK4eX2ytVOLgeTcmDWhFBrEkYAYV2ldS2N9cXt6jbVKlREls8vW2zap7frdY5/ExILH1OSzyShVBig1tdrobIU2V3WJIINE1sMu8o0AlkhNocMbECdQdB1oGlQL6I38oTjBCkvA4om6F8spzci/dLgUQ2NcMZQZeqa+Ci2Jwp1VRjI4TJGAdufw02hNCkmjtSU+2e7gehpoZinl6n2c5kayZ0im7NerbN0Bq3H4QyU5sVl1YhUNh6nEiqmFB7dN7ADwNZhgblIZKxwvDtc7G3NcPLKTDqVJ6RnS64G2VnOiA0eKBrnTCVjbJHgGHomgwcu/8g69hgTibe4TLfnHEU59naIGpfqMjz8O9OsP1EnOArCvoCI9082wMN6WzBWPcZpZTh2sdnGgwgdzAcPrf7diobPDTTBxN7oAHIu6RZkI4g9gAw48XB8zPfTQtRLQ7tdOXu+elyxul0RT5R9IVGZwGTBJLtAXeuRqeaPBWSCQiBIl4o5gVxKN8lHaIYVe3+EkH4nI26we4WfDfFIKlF+pJMEfj99XTu0pzdtTOv5p2Ctckqpo59qRkqXk4UuSoocww/yImlZYJQJga52obwYGUuTXhBMUbDbItJlAlSnRc/LasW6oBKpnJB2Sc902WZLL+uxTXK584sE8v5aieDgiBMbn6sp8mGXkNOxmRRKBR6Ai7rDKT2mFMfLwZ4UQznqizav3Aed7f47MXOsUo5rBiv157oP80tH7P5KWIWYRjcP1m4UDS26DyKA6Vuttef7D9NMRdT+DQxi1PkFgqf075soo2P8ZzZuKw391jvRflkcsrzmmJfZJ/ZRa9orK1Vw1rXwdD+urpY4yEFVVcF6vs7NWBnRJ/IxTCO6XC4soavBOyk8pK7Qna6al9jRcOOysEWvgm3YzZLYcBmDmEEUxI02WraGVnHlsKOnQlTLLypYp89VhoVSrZGS9rF0kBIiBYH+ermppH8Mk0tS9K5lO6PhMOp7HG+9mD9By8UuH9L60/cqLv61qr3IrB8XcnK7e2rT8GKtUOrzqyzhQbfxK2JU2X3P/zy48vjY1u+Kvvr1PrS548sWX7+eNvt8uHiw90H7pZ0PPz59tDdzy43//OKl14ZkN6v+m/7yPVXT7+zdtnZ06vXkJe3fHvmxOUXw217f/hoY/Rm5txP1dIF3vvvtbpBY/jolT0Dw9ID7/Kd/uAn12oKA9veuH9yYuOmTMvKe03HV/12aZl+oPzTo398/dLNwpNXS7/4ZWz8m79vbAvGx+sPrLh6uPv3MeW74Q339hzr6ft1RX916K75/LN31vwZjB97Dh2SWpNnH+0jXeUPWZ2PHi0t2Hyrd2LrkoKC/wFNFp9z
@@ -1 +1 @@
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
@@ -1 +0,0 @@
eNqNVX1sE2UY3xgkOiAQVFSUcDYogrvu2l63dcboKGwO2IdrHYxl1Ld3b9tj17vj3ve6DkLCV4JR+Tid8vGHICutlAqbbqAyTAbRQSCIxIhDlIQ/FELUEUKIRjPfu7X7Fu0/vXvf5+P3/H7P89ymRASqSJCl7JQgYagCDpMXpG9KqHCNBhHeEg9DHJL5WHWVx9uqqULv0yGMFVScnw8UwQokHFJlReCsnBzOj9jywxAhEIQo5pf55is5U9ZZwiDqw3IjlJClmLIxdjaPsmSsyEn9Oosqi5A8WTQEVQu55WQCRcLGUVMI4HmIwiFINUFA/lRKkCgUeNGyPo8a8gQICQgTMCPdSWwMo2Ygr0wFIR4VKCCrYWAUTJEnygMkqlQFEicgTs6jyimOHBBMw518CAKVC1EBTTKZslLLSNQwpAIQk2NM4I6J2yxrVgMXblZMsCYkE/7giSyLPpLJsJJA2DwbmdC4ESRFM2pZZ+EE3GzYjEL8qqfEiGsR+ExQzcfY6rx8nX+h2x6pq8OFfjfkAjjkbLKsbxjB4Fju60fhUyHSxFEEWzyaJDU/ZVaXrsH3v7IbKJEPqqqsEusAEBEkgBqM1pB5KBoROBFoPKQdtJNGsiRBTNtJ8zAFdiaTLt0+/84YDxGnCoqhhAk2rR3RZJikg9T6EBeCYWAyrJCmhioWzBYdJHyIEYRVQQpa1ht0G5MiqNAou37AsmGY2rJ/NeSI3qS8RAgCnozbjlhIRlhvHzNARwHHQQXTUOJknsTXPwquFZQ8iocBEWCY5AwezL7Tk40QKjQQhQiMD3jpbUBRRIEzGy9/NeEslRaKNrCMvU4amtNkDCWsHy/J4MivbibzLlGM1cFa7W1RmsyVIIlkYGkREEhxxbw/MfxCAVwjiUOnd4keH3A+MtxGRvrBCsBVeUaENATRDwI1XMB+Mvxc1SQshKGecFePTZe+HErnsNpsVlf7iMCoWeL0g2ZntQ+SPOiSJL3koJkCmrEdybAkQimIQ3prYRHzIWl2hexBuDlOQmINbYoRReD5M4n05jpQtTSj5k9ZM2KLiDr6SW9Iy6PsBZQHKpTRq5TNUewoKLY7qLIKb8qdTuMdV4x2L5ljFCCCLM6In+BCmtQI+aR7XNl7LUNlqSS/KIQFTKfXNhHLeNVjLMMwvc/c11IlTS9IRsaYw+Vy/UdcwgzEeodRH824aHuBd6BKJ7uylxrPc2D3p/HEDTwE0dz7WA7hyVhT97UeH4/dsTKZBk0LvN5Fnsk6WuYvjIZKK/2SVBtdGaldoYJFTtvazijNibLG05h8ACFtNkQU670Uy7N8EWPjgJ9leSfJUcAGGMj7eQY4bX4H1xoRgJ60WW1UUJaDIjzqLqXdgKwR2mO2jZ5YVFdZUlHuTq2ga2S/TPjzAsKzJEsw7oEqaUc9aaYmA67COHGvKanTO4o4lwOwhUU8LHJwLOukFy+vacs00GCDxIztYH5oN8YHFtKX2ZVz3nwgy/zleKu7l+S8MuWvd3cvza8/1El/+mhJd01f7cXNL5dfi/9Y5nvnz7eDDUv6G449e2P+aT7w2M9b2rtn3TvWsE1atWpX07mqfZ7vnaee3P71nZ6K7tyTzytH/Y4r9trdzrsT9yxGM3py711RTs1yH7jU1xcpmr33jbZpPa23efaJHW2n9l/P2373h5uF3/5x+Livcts0/uqG3Oi515a5Onb2LlEPW6/qZ+svTJ/03ty+lvKFHfunT029jpe39W3M7t57KHq1ZeJU7eQXx7femJ06MXmVp2nq5FtrAtTNrm0fT6r86qLwOLvz+vbuRyIHyvZ3zuy/XIJ+n/D3/I5vuubtWNC24aUJdMI9Z2LPjV9feP/ac+zl0wsce3K/O3Mp1cV379t6uww8+FZnS8uCvF2dtx4qdT3s++y3FVNuncW1H8zsP1fR88udQkJcf39O1ucX2Mbz2VlZ/wBwZ4U6
@@ -1 +1 @@
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
@@ -1 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -1 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
# Adding nodes as dataset examples in Studio
In LangGraph Studio you can create dataset examples from the thread history in the right-hand pane. This can be especially useful when you want to evaluate intermediate steps of the agent.
1. Click on the `Add to Dataset` button to enter the dataset mode.
1. Select nodes which you want to add to dataset.
1. Select the target dataset to create the example in.
You can edit the example payload before sending it to the dataset, which is useful if you need to make changes to conform the example to the dataset schema.
Finally, you can customise the target dataset by clicking on the `Settings` button.
See [Evaluating intermediate steps](https://docs.smith.langchain.com/evaluation/how_to_guides/langgraph#evaluating-intermediate-steps) for more details on how to evaluate intermediate steps.
<video controls allowfullscreen="true" poster="../img/studio_datasets.jpg">
<source src="https://langgraph-docs-assets.pages.dev/studio_datasets.mp4" type="video/mp4">
</video>
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+1
View File
@@ -258,3 +258,4 @@ RUN set -ex && \
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
```
+132
View File
@@ -0,0 +1,132 @@
# Breakpoints
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](./human_in_the_loop.md#interrupt) for this purpose.
## Requirements
To use breakpoints, you will need to:
1. [**Specify a checkpointer**](persistence.md#checkpoints) to save the graph state after each step.
2. [**Set breakpoints**](#setting-breakpoints) to specify where execution should pause.
3. **Run the graph** with a [**thread ID**](./persistence.md#threads) to pause execution at the breakpoint.
4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](./human_in_the_loop.md#the-command-primitive)).
## Setting breakpoints
There are two places where you can set breakpoints:
1. **Before** or **after** a node executes by setting breakpoints at **compile time** or **run time**. We call these [**static breakpoints**](#static-breakpoints).
2. **Inside** a node using the [`NodeInterrupt` exception](#nodeinterrupt-exception).
### Static breakpoints
Static breakpoints are triggered either **before** or **after** a node executes. You can set static breakpoints by specifying `interrupt_before` and `interrupt_after` at **"compile" time** or **run time**.
=== "Compile time"
```python
graph = graph_builder.compile(
interrupt_before=["node_a"],
interrupt_after=["node_b", "node_c"],
checkpointer=..., # Specify a checkpointer
)
thread_config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=thread_config)
# Optionally update the graph state based on user input
graph.update_state(update, config=thread_config)
# Resume the graph
graph.invoke(None, config=thread_config)
```
=== "Run time"
```python
graph.invoke(
inputs,
config={"configurable": {"thread_id": "some_thread"}},
interrupt_before=["node_a"],
interrupt_after=["node_b", "node_c"]
)
thread_config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=thread_config)
# Optionally update the graph state based on user input
graph.update_state(update, config=thread_config)
# Resume the graph
graph.invoke(None, config=thread_config)
```
!!! note
You cannot set static breakpoints at runtime for **sub-graphs**.
If you have a sub-graph, you must set the breakpoints at compilation time.
Static breakpoints can be especially useful for debugging if you want to step through the graph execution one
node at a time or if you want to pause the graph execution at specific nodes.
### `NodeInterrupt` exception
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
??? node "`NodeInterrupt` exception"
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
```python
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
```
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
```python
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
```python
# Update the state to pass the dynamic breakpoint
graph.update_state(config=thread_config, values={"input": "foo"})
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
```python
# This update will skip the node `my_node` altogether
graph.update_state(config=thread_config, values=None, as_node="my_node")
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
## Additional Resources 📚
- [**Conceptual Guide: Persistence**](persistence.md): Read the persistence guide for more context about persistence.
- [**Conceptual Guide: Human-in-the-loop**](human_in_the_loop.md): Read the human-in-the-loop guide for more context on integrating human feedback into LangGraph applications using breakpoints.
- [**How to View and Update Past Graph State**](../how-tos/human_in_the_loop/time-travel.ipynb): Step-by-step instructions for working with graph state that demonstrate the **replay** and **fork** actions.
+636 -214
View File
@@ -1,322 +1,744 @@
# Human-in-the-loop
Human-in-the-loop (or "on-the-loop") enhances agent capabilities through several common user interaction patterns.
!!! tip "This guide uses the new `interrupt` function."
Common interaction patterns include:
As of LangGraph 0.2.57, the recommended way to set breakpoints is using the [`interrupt` function][langgraph.types.interrupt] as it simplifies **human-in-the-loop** patterns.
(1) `Approval` - We can interrupt our agent, surface the current state to a user, and allow the user to accept an action.
If you're looking for the previous version of this conceptual guide, which relied on static breakpoints and `NodeInterrupt` exception, it is available [here](v0-human-in-the-loop.md).
(2) `Editing` - We can interrupt our agent, surface the current state to a user, and allow the user to edit the agent state.
A **human-in-the-loop** (or "on-the-loop") workflow integrates human input into automated processes, allowing for decisions, validation, or corrections at key stages. This is especially useful in **LLM-based applications**, where the underlying model may generate occasional inaccuracies. In low-error-tolerance scenarios like compliance, decision-making, or content generation, human involvement ensures reliability by enabling review, correction, or override of model outputs.
(3) `Input` - We can explicitly create a graph node to collect human input and pass that input directly to the agent state.
Use-cases for these interaction patterns include:
## Use cases
(1) `Reviewing tool calls` - We can interrupt an agent to review and edit the results of tool calls.
Key use cases for **human-in-the-loop** workflows in LLM-based applications include:
(2) `Time Travel` - We can manually re-play and / or fork past actions of an agent.
1. [**🛠️ Reviewing tool calls**](#review-tool-calls): Humans can review, edit, or approve tool calls requested by the LLM before tool execution.
2. **✅ Validating LLM outputs**: Humans can review, edit, or approve content generated by the LLM.
3. **💡 Providing context**: Enable the LLM to explicitly request human input for clarification or additional details or to support multi-turn conversations.
## Persistence
## `interrupt`
All of these interaction patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which will write a checkpoint of the graph state at each step. Persistence allows the graph to stop so that a human can review and / or edit the current state of the graph and then resume with the human's input.
### Breakpoints
Adding a [breakpoint](./low_level.md#breakpoints) a specific location in the graph flow is one way to enable human-in-the-loop. In this case, the developer knows *where* in the workflow human input is needed and simply places a breakpoint prior to or following that particular graph node.
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. This function is useful for tasks like approvals, edits, or collecting additional input. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`](../reference/types.md#langgraph.types.Command) object to resume the graph with a value provided by the human.
```python
# Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["step_for_human_in_the_loop"])
from langgraph.types import interrupt
# Run the graph up to the breakpoint
thread_config = {"configurable": {"thread_id": "1"}}
for event in graph.stream(inputs, thread_config, stream_mode="values"):
print(event)
def human_node(state: State):
value = interrupt(
# Any JSON serializable value to surface to the human.
# For example, a question or a piece of text or a set of keys in the state
{
"text_to_revise": state["some_text"]
}
)
# Update the state with the human's input or route the graph based on the input.
return {
"some_text": value
}
graph = graph_builder.compile(
checkpointer=checkpointer # Required for `interrupt` to work
)
# Run the graph until the interrupt
thread_config = {"configurable": {"thread_id": "some_id"}}
graph.invoke(some_input, config=thread_config)
# Perform some action that requires human in the loop
# Continue the graph execution from the current checkpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
# Resume the graph with the human's input
graph.invoke(Command(resume=value_from_human), config=thread_config)
```
### Dynamic Breakpoints
```pycon
{'some_text': 'Edited text'}
```
Alternatively, the developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
!!! warning
Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used.
For this reason, interrupts are typically best placed at the start of a node or in a dedicated node. Please read the [resuming from an interrupt](#how-does-resuming-from-an-interrupt-work) section for more details.
??? "Full Code"
Here's a full example of how to use `interrupt` in a graph, if you'd like
to see the code in action.
```python
from typing import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
class State(TypedDict):
"""The graph state."""
some_text: str
def human_node(state: State):
value = interrupt(
# Any JSON serializable value to surface to the human.
# For example, a question or a piece of text or a set of keys in the state
{
"text_to_revise": state["some_text"]
}
)
return {
# Update the state with the human's input
"some_text": value
}
# Build the graph
graph_builder = StateGraph(State)
# Add the human-node to the graph
graph_builder.add_node("human_node", human_node)
graph_builder.add_edge(START, "human_node")
# A checkpointer is required for `interrupt` to work.
checkpointer = MemorySaver()
graph = graph_builder.compile(
checkpointer=checkpointer
)
# Pass a thread ID to the graph to run it.
thread_config = {"configurable": {"thread_id": uuid.uuid4()}}
# Using stream() to directly surface the `__interrupt__` information.
for chunk in graph.stream({"some_text": "Original text"}, config=thread_config):
print(chunk)
# Resume using Command
for chunk in graph.stream(Command(resume="Edited text"), config=thread_config):
print(chunk)
```
```pycon
{'__interrupt__': (
Interrupt(
value={'question': 'Please revise the text', 'some_text': 'Original text'},
resumable=True,
ns=['human_node:10fe492f-3688-c8c6-0d0a-ec61a43fecd6'],
when='during'
),
)
}
{'human_node': {'some_text': 'Edited text'}}
```
## Requirements
To use `interrupt` in your graph, you need to:
1. [**Specify a checkpointer**](persistence.md#checkpoints) to save the graph state after each step.
2. **Call `interrupt()`** in the appropriate place. See the [Design Patterns](#design-patterns) section for examples.
3. **Run the graph** with a [**thread ID**](./persistence.md#threads) until the `interrupt` is hit.
4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](#the-command-primitive)).
## Design Patterns
There are typically three different **actions** that you can do with a human-in-the-loop workflow:
1. **Approve or Reject**: Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. This pattern often involve **routing** the graph based on the human's input.
2. **Edit Graph State**: Pause the graph to review and edit the graph state. This is useful for correcting mistakes or updating the state with additional information. This pattern often involves **updating** the state with the human's input.
3. **Get Input**: Explicitly request human input at a particular step in the graph. This is useful for collecting additional information or context to inform the agent's decision-making process or for supporting **multi-turn conversations**.
Below we show different design patterns that can be implemented using these **actions**.
### Approve or Reject
<figure markdown="1">
![image](img/human_in_the_loop/approve-or-reject.png){: style="max-height:400px"}
<figcaption>Depending on the human's approval or rejection, the graph can proceed with the action or take an alternative path.</figcaption>
</figure>
Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action.
```python
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
from typing import Literal
from langgraph.types import interrupt, Command
def human_approval(state: State) -> Command[Literal["some_node", "another_node"]]:
is_approved = interrupt(
{
"question": "Is this correct?",
# Surface the output that should be
# reviewed and approved by the human.
"llm_output": state["llm_output"]
}
)
if is_approved:
return Command(goto="some_node")
else:
return Command(goto="another_node")
# Add the node to the graph in an appropriate location
# and connect it to the relevant nodes.
graph_builder.add_node("human_approval", human_approval)
graph = graph_builder.compile(checkpointer=checkpointer)
# After running the graph and hitting the interrupt, the graph will pause.
# Resume it with either an approval or rejection.
thread_config = {"configurable": {"thread_id": "some_id"}}
graph.invoke(Command(resume=True), config=thread_config)
```
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
See [how to review tool calls](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a more detailed example.
### Review & Edit State
<figure markdown="1">
![image](img/human_in_the_loop/edit-graph-state-simple.png){: style="max-height:400px"}
<figcaption>A human can review and edit the state of the graph. This is useful for correcting mistakes or updating the state with additional information.
</figcaption>
</figure>
```python
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
from langgraph.types import interrupt
def human_editing(state: State):
...
result = interrupt(
# Interrupt information to surface to the client.
# Can be any JSON serializable value.
{
"task": "Review the output from the LLM and make any necessary edits.",
"llm_generated_summary": state["llm_generated_summary"]
}
)
# Update the state with the edited text
return {
"llm_generated_summary": result["edited_text"]
}
# Add the node to the graph in an appropriate location
# and connect it to the relevant nodes.
graph_builder.add_node("human_editing", human_editing)
graph = graph_builder.compile(checkpointer=checkpointer)
...
# After running the graph and hitting the interrupt, the graph will pause.
# Resume it with the edited text.
thread_config = {"configurable": {"thread_id": "some_id"}}
graph.invoke(
Command(resume={"edited_text": "The edited text"}),
config=thread_config
)
```
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
See [How to wait for user input using interrupt](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a more detailed example.
```python
# Update the state to pass the dynamic breakpoint
graph.update_state(config=thread_config, values={"input": "foo"})
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
### Review Tool Calls
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
<figure markdown="1">
![image](img/human_in_the_loop/tool-call-review.png){: style="max-height:400px"}
<figcaption>A human can review and edit the output from the LLM before proceeding. This is particularly
critical in applications where the tool calls requested by the LLM may be sensitive or require human oversight.
</figcaption>
</figure>
```python
# This update will skip the node `my_node` altogether
graph.update_state(config=thread_config, values=None, as_node="my_node")
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]:
# This is the value we'll be providing via Command(resume=<human_review>)
human_review = interrupt(
{
"question": "Is this correct?",
# Surface tool calls for review
"tool_call": tool_call
}
)
review_action, review_data = human_review
# Approve the tool call and continue
if review_action == "continue":
return Command(goto="run_tool")
# Modify the tool call manually and then continue
elif review_action == "update":
...
updated_msg = get_updated_msg(review_data)
# Remember that to modify an existing message you will need
# to pass the message with a matching ID.
return Command(goto="run_tool", update={"messages": [updated_message]})
# Give natural language feedback, and then pass that back to the agent
elif review_action == "feedback":
...
feedback_msg = get_feedback_msg(review_data)
return Command(goto="call_llm", update={"messages": [feedback_msg]})
```
See [our guide](../how-tos/human_in_the_loop/dynamic_breakpoints.ipynb) for a detailed how-to on doing this!
See [how to review tool calls](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a more detailed example.
## Interaction Patterns
### Multi-turn conversation
### Approval
<figure markdown="1">
![image](img/human_in_the_loop/multi-turn-conversation.png){: style="max-height:400px"}
<figcaption>A <strong>multi-turn conversation</strong> architecture where an <strong>agent</strong> and <strong>human node</strong> cycle back and forth until the agent decides to hand off the conversation to another agent or another part of the system.
</figcaption>
</figure>
![](./img/human_in_the_loop/approval.png)
A **multi-turn conversation** involves multiple back-and-forth interactions between an agent and a human, which can allow the agent to gather additional information from the human in a conversational manner.
Sometimes we want to approve certain steps in our agent's execution.
We can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to the step that we want to approve.
This design pattern is useful in an LLM application consisting of [multiple agents](./multi_agent.md). One or more agents may need to carry out multi-turn conversations with a human, where the human provides input or feedback at different stages of the conversation. For simplicity, the agent implementation below is illustrated as a single node, but in reality
it may be part of a larger graph consisting of multiple nodes and include a conditional edge.
This is generally recommend for sensitive actions (e.g., using external APIs or writing to a database).
With persistence, we can surface the current agent state as well as the next step to a user for review and approval.
If approved, the graph resumes execution from the last saved checkpoint, which is saved to the `thread`:
=== "Using a human node per agent"
In this pattern, each agent has its own human node for collecting user input.
This can be achieved by either naming the human nodes with unique names (e.g., "human for agent 1", "human for agent 2") or by
using subgraphs where a subgraph contains a human node and an agent node.
```python
from langgraph.types import interrupt
def human_input(state: State):
human_message = interrupt("human_input")
return {
"messages": [
{
"role": "human",
"content": human_message
}
]
}
def agent(state: State):
# Agent logic
...
graph_builder.add_node("human_input", human_input)
graph_builder.add_edge("human_input", "agent")
graph = graph_builder.compile(checkpointer=checkpointer)
# After running the graph and hitting the interrupt, the graph will pause.
# Resume it with the human's input.
graph.invoke(
Command(resume="hello!"),
config=thread_config
)
```
=== "Sharing human node across multiple agents"
In this pattern, a single human node is used to collect user input for multiple agents. The active agent is determined from the state, so after human input is collected, the graph can route to the correct agent.
```python
from langgraph.types import interrupt
def human_node(state: MessagesState) -> Command[Literal["agent_1", "agent_2", ...]]:
"""A node for collecting user input."""
user_input = interrupt(value="Ready for user input.")
# Determine the **active agent** from the state, so
# we can route to the correct agent after collecting input.
# For example, add a field to the state or use the last active agent.
# or fill in `name` attribute of AI messages generated by the agents.
active_agent = ...
return Command(
update={
"messages": [{
"role": "human",
"content": user_input,
}]
},
goto=active_agent,
)
```
See [how to implement multi-turn conversations](../how-tos/multi-agent-multi-turn-convo.ipynb) for a more detailed example.
### Validating human input
If you need to validate the input provided by the human within the graph itself (rather than on the client side), you can achieve this by using multiple interrupt calls within a single node.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to approve
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
from langgraph.types import interrupt
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# ... Get human approval ...
def human_node(state: State):
"""Human node with validation."""
question = "What is your age?"
# If approved, continue the graph execution from the last saved checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
while True:
answer = interrupt(question)
# Validate answer, if the answer isn't valid ask for input again.
if not isinstance(answer, int) or answer < 0:
question = f"'{answer} is not a valid age. What is your age?"
answer = None
continue
else:
# If the answer is valid, we can proceed.
break
print(f"The human in the loop is {answer} years old.")
return {
"age": answer
}
```
See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed how-to on doing this!
## The `Command` primitive
### Editing
When using the `interrupt` function, the graph will pause at the interrupt and wait for user input.
![](./img/human_in_the_loop/edit_graph_state.png)
Graph execution can be resumed using the [Command](../reference/types.md#langgraph.types.Command) primitive which can be passed through the `invoke`, `ainvoke`, `stream` or `astream` methods.
Sometimes we want to review and edit the agent's state.
As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to the step we want to check.
We can surface the current state to a user and allow the user to edit the agent state.
This can, for example, be used to correct the agent if it made a mistake (e.g., see the section on tool calling below).
The `Command` primitive provides several options to control and modify the graph's state during resumption:
We can edit the graph state by forking the current checkpoint, which is saved to the `thread`.
1. **Pass a value to the `interrupt`**: Provide data, such as a user's response, to the graph using `Command(resume=value)`. Execution resumes from the beginning of the node where the `interrupt` was used, however, this time the `interrupt(...)` call will return the value passed in the `Command(resume=value)` instead of pausing the graph.
We can then proceed with the graph from our forked checkpoint as done before.
```python
# Resume graph execution with the user's input.
graph.invoke(Command(resume={"age": "25"}), thread_config)
```
2. **Update the graph state**: Modify the graph state using `Command(update=update)`. Note that resumption starts from the beginning of the node where the `interrupt` was used. Execution resumes from the beginning of the node where the `interrupt` was used, but with the updated state.
```python
# Update the graph state and resume.
# You must provide a `resume` value if using an `interrupt`.
graph.invoke(Command(update={"foo": "bar"}, resume="Let's go!!!"), thread_config)
```
By leveraging `Command`, you can resume graph execution, handle user inputs, and dynamically adjust the graph's state.
## Using with `invoke` and `ainvoke`
When you use `stream` or `astream` to run the graph, you will receive an `Interrupt` event that let you know the `interrupt` was triggered.
`invoke` and `ainvoke` do not return the interrupt information. To access this information, you must use the [get_state](../reference/graphs.md#langgraph.graph.graph.CompiledGraph.get_state) method to retrieve the graph state after calling `invoke` or `ainvoke`.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to review
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the state, decide to edit it, and create a forked checkpoint with the new state
graph.update_state(thread, {"state": "new state"})
# Continue the graph execution from the forked checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
# Run the graph up to the interrupt
result = graph.invoke(inputs, thread_config)
# Get the graph state to get interrupt information.
state = graph.get_state(thread_config)
# Print the state values
print(state.values)
# Print the pending tasks
print(state.tasks)
# Resume the graph with the user's input.
graph.invoke(Command(resume={"age": "25"}), thread_config)
```
See [this guide](../how-tos/human_in_the_loop/edit-graph-state.ipynb) for a detailed how-to on doing this!
```pycon
{'foo': 'bar'} # State values
(
PregelTask(
id='5d8ffc92-8011-0c9b-8b59-9d3545b7e553',
name='node_foo',
path=('__pregel_pull', 'node_foo'),
error=None,
interrupts=(Interrupt(value='value_in_interrupt', resumable=True, ns=['node_foo:5d8ffc92-8011-0c9b-8b59-9d3545b7e553'], when='during'),), state=None,
result=None
),
) # Pending tasks. interrupts
```
### Input
## How does resuming from an interrupt work?
![](./img/human_in_the_loop/wait_for_input.png)
!!! warning
Sometimes we want to explicitly get human input at a particular step in the graph.
We can create a graph node designated for this (e.g., `human_input` in our example diagram).
As with approval and editing, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to this node.
We can then perform a state update that includes the human input, just as we did with editing state.
Resuming from an `interrupt` is **different** from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called.
But, we add one thing:
A critical aspect of using `interrupt` is understanding how resuming works. When you resume execution after an `interrupt`, graph execution starts from the **beginning** of the **graph node** where the last `interrupt` was triggered.
We can use `as_node=human_input` with the state update to specify that the state update *should be treated as a node*.
The is subtle, but important:
With editing, the user makes a decision about whether or not to edit the graph state.
With input, we explicitly define a node in our graph for collecting human input!
The state update with the human input then runs *as this node*.
**All** code from the beginning of the node to the `interrupt` will be re-executed.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to to collect human input
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_input"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Update the state with the user input as if it was the human_input node
graph.update_state(thread, {"user_input": user_input}, as_node="human_input")
# Continue the graph execution from the checkpoint created by the human_input node
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
counter = 0
def node(state: State):
# All the code from the beginning of the node to the interrupt will be re-executed
# when the graph resumes.
global counter
counter += 1
print(f"> Entered the node: {counter} # of times")
# Pause the graph and wait for user input.
answer = interrupt()
print("The value of counter is:", counter)
...
```
See [this guide](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a detailed how-to on doing this!
Upon **resuming** the graph, the counter will be incremented a second time, resulting in the following output:
## Use-cases
```pycon
> Entered the node: 2 # of times
The value of counter is: 2
```
### Reviewing Tool Calls
## Common Pitfalls
Some user interaction patterns combine the above ideas.
### Side-effects
For example, many agents use [tool calling](https://python.langchain.com/docs/how_to/tool_calling/) to make decisions.
Place code with side effects, such as API calls, **after** the `interrupt` to avoid duplication, as these are re-triggered every time the node is resumed.
Tool calling presents a challenge because the agent must get two things right:
=== "Side effects before interrupt (BAD)"
(1) The name of the tool to call
This code will re-execute the API call another time when the node is resumed from
the `interrupt`.
(2) The arguments to pass to the tool
This can be problematic if the API call is not idempotent or is just expensive.
Even if the tool call is correct, we may also want to apply discretion:
```python
from langgraph.types import interrupt
(3) The tool call may be a sensitive operation that we want to approve
def human_node(state: State):
"""Human node with validation."""
api_call(...) # This code will be re-executed when the node is resumed.
answer = interrupt(question)
```
With these points in mind, we can combine the above ideas to create a human-in-the-loop review of a tool call.
=== "Side effects after interrupt (OK)"
```python
from langgraph.types import interrupt
def human_node(state: State):
"""Human node with validation."""
answer = interrupt(question)
api_call(answer) # OK as it's after the interrupt
```
=== "Side effects in a separate node (OK)"
```python
from langgraph.types import interrupt
def human_node(state: State):
"""Human node with validation."""
answer = interrupt(question)
return {
"answer": answer
}
def api_call_node(state: State):
api_call(...) # OK as it's in a separate node
```
### Subgraphs called as functions
When invoking a subgraph [as a function](low_level.md#as-a-function), the **parent graph** will resume execution from the **beginning of the node** where the subgraph was invoked (and where an `interrupt` was triggered). Similarly, the **subgraph**, will resume from the **beginning of the node** where the `interrupt()` function was called.
For example,
```python
# Compile our graph with a checkpointer and a breakpoint before the step to to review the tool call from the LLM
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_review"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the tool call and update it, if needed, as the human_review node
graph.update_state(thread, {"tool_call": "updated tool call"}, as_node="human_review")
# Otherwise, approve the tool call and proceed with the graph execution with no edits
# Continue the graph execution from either:
# (1) the forked checkpoint created by human_review or
# (2) the checkpoint saved when the tool call was originally made (no edits in human_review)
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
def node_in_parent_graph(state: State):
some_code() # <-- This will re-execute when the subgraph is resumed.
# Invoke a subgraph as a function.
# The subgraph contains an `interrupt` call.
subgraph_result = subgraph.invoke(some_input)
...
```
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a detailed how-to on doing this!
??? "**Example: Parent and Subgraph Execution Flow**"
### Time Travel
Say we have a parent graph with 3 nodes:
When working with agents, we often want closely examine their decision making process:
**Parent Graph**: `node_1` → `node_2` (subgraph call) → `node_3`
(1) Even when they arrive a desired final result, the reasoning that led to that result is often important to examine.
And the subgraph has 3 nodes, where the second node contains an `interrupt`:
(2) When agents make mistakes, it is often valuable to understand why.
**Subgraph**: `sub_node_1` → `sub_node_2` (`interrupt`) → `sub_node_3`
(3) In either of the above cases, it is useful to manually explore alternative decision making paths.
When resuming the graph, the execution will proceed as follows:
Collectively, we call these debugging concepts `time-travel` and they are composed of `replaying` and `forking`.
1. **Skip `node_1`** in the parent graph (already executed, graph state was saved in snapshot).
2. **Re-execute `node_2`** in the parent graph from the start.
3. **Skip `sub_node_1`** in the subgraph (already executed, graph state was saved in snapshot).
4. **Re-execute `sub_node_2`** in the subgraph from the beginning.
5. Continue with `sub_node_3` and subsequent nodes.
#### Replaying
Here is abbreviated example code that you can use to understand how subgraphs work with interrupts.
It counts the number of times each node is entered and prints the count.
![](./img/human_in_the_loop/replay.png)
```python
import uuid
from typing import TypedDict
Sometimes we want to simply replay past actions of an agent.
Above, we showed the case of executing an agent from the current state (or checkpoint) of the graph.
from langgraph.graph import StateGraph
from langgraph.constants import START
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver
We by simply passing in `None` for the input with a `thread`.
```
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
class State(TypedDict):
"""The graph state."""
state_counter: int
Now, we can modify this to replay past actions from a *specific* checkpoint by passing in the checkpoint ID.
To get a specific checkpoint ID, we can easily get all of the checkpoints in the thread and filter to the one we want.
counter_node_in_subgraph = 0
```python
all_checkpoints = []
for state in app.get_state_history(thread):
all_checkpoints.append(state)
```
def node_in_subgraph(state: State):
"""A node in the sub-graph."""
global counter_node_in_subgraph
counter_node_in_subgraph += 1 # This code will **NOT** run again!
print(f"Entered `node_in_subgraph` a total of {counter_node_in_subgraph} times")
Each checkpoint has a unique ID, which we can use to replay from a specific checkpoint.
counter_human_node = 0
Assume from reviewing the checkpoints that we want to replay from one, `xxx`.
def human_node(state: State):
global counter_human_node
counter_human_node += 1 # This code will run again!
print(f"Entered human_node in sub-graph a total of {counter_human_node} times")
answer = interrupt("what is your name?")
print(f"Got an answer of {answer}")
We just pass in the checkpoint ID when we run the graph.
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
Importantly, the graph knows which checkpoints have been previously executed.
checkpointer = MemorySaver()
So, it will re-play any previously executed nodes rather than re-executing them.
subgraph_builder = StateGraph(State)
subgraph_builder.add_node("some_node", node_in_subgraph)
subgraph_builder.add_node("human_node", human_node)
subgraph_builder.add_edge(START, "some_node")
subgraph_builder.add_edge("some_node", "human_node")
subgraph = subgraph_builder.compile(checkpointer=checkpointer)
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay) for related context on replaying.
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
counter_parent_node = 0
#### Forking
def parent_node(state: State):
"""This parent node will invoke the subgraph."""
global counter_parent_node
![](./img/human_in_the_loop/forking.png)
counter_parent_node += 1 # This code will run again on resuming!
print(f"Entered `parent_node` a total of {counter_parent_node} times")
# Please note that we're intentionally incrementing the state counter
# in the graph state as well to demonstrate that the subgraph update
# of the same key will not conflict with the parent graph (until
subgraph_state = subgraph.invoke(state)
return subgraph_state
Sometimes we want to fork past actions of an agent, and explore different paths through the graph.
`Editing`, as discussed above, is *exactly* how we do this for the *current* state of the graph!
builder = StateGraph(State)
builder.add_node("parent_node", parent_node)
builder.add_edge(START, "parent_node")
But, what if we want to fork *past* states of the graph?
# A checkpointer must be enabled for interrupts to work!
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
For example, let's say we want to edit a particular checkpoint, `xxx`.
config = {
"configurable": {
"thread_id": uuid.uuid4(),
}
}
We pass this `checkpoint_id` when we update the state of the graph.
for chunk in graph.stream({"state_counter": 1}, config):
print(chunk)
```python
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xxx"}}
graph.update_state(config, {"state": "updated state"}, )
```
print('--- Resuming ---')
This creates a new forked checkpoint, `xxx-fork`, which we can then run the graph from.
for chunk in graph.stream(Command(resume="35"), config):
print(chunk)
```
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx-fork'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
This will print out
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state) for related context on forking.
```pycon
--- First invocation ---
In parent node: {'foo': 'bar'}
Entered `parent_node` a total of 1 times
Entered `node_in_subgraph` a total of 1 times
Entered human_node in sub-graph a total of 1 times
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:0b23d72f-aaba-0329-1a59-ca4f3c8bad3b', 'human_node:25df717c-cb80-57b0-7410-44e20aac8f3c'], when='during'),)}
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
--- Resuming ---
In parent node: {'foo': 'bar'}
Entered `parent_node` a total of 2 times
Entered human_node in sub-graph a total of 2 times
Got an answer of 35
{'parent_node': None}
```
### Using multiple interrupts
Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validating-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully.
When a node contains multiple interrupt calls, LangGraph keeps a list of resume values specific to the task executing the node. Whenever execution resumes, it starts at the beginning of the node. For each interrupt encountered, LangGraph checks if a matching value exists in the task's resume list. Matching is **strictly index-based**, so the order of interrupt calls within the node is critical.
To avoid issues, refrain from dynamically changing the node's structure between executions. This includes adding, removing, or reordering interrupt calls, as such changes can result in mismatched indices. These problems often arise from unconventional patterns, such as mutating state via `Command(resume=..., update=SOME_STATE_MUTATION)` or relying on global variables to modify the nodes structure dynamically.
??? "Example of incorrect code"
```python
import uuid
from typing import TypedDict, Optional
from langgraph.graph import StateGraph
from langgraph.constants import START
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver
class State(TypedDict):
"""The graph state."""
age: Optional[str]
name: Optional[str]
def human_node(state: State):
if not state.get('name'):
name = interrupt("what is your name?")
else:
name = "N/A"
if not state.get('age'):
age = interrupt("what is your age?")
else:
age = "N/A"
print(f"Name: {name}. Age: {age}")
return {
"age": age,
"name": name,
}
builder = StateGraph(State)
builder.add_node("human_node", human_node)
builder.add_edge(START, "human_node")
# A checkpointer must be enabled for interrupts to work!
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
"configurable": {
"thread_id": uuid.uuid4(),
}
}
for chunk in graph.stream({"age": None, "name": None}, config):
print(chunk)
for chunk in graph.stream(Command(resume="John", update={"name": "foo"}), config):
print(chunk)
```
```pycon
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)}
Name: N/A. Age: John
{'human_node': {'age': 'John', 'name': 'N/A'}}
```
## Additional Resources 📚
- [**Conceptual Guide: Persistence**](persistence.md#replay): Read the persistence guide for more context on replaying.
- [**How to Guides: Human-in-the-loop**](../how-tos/index.md#human-in-the-loop): Learn how to implement human-in-the-loop workflows in LangGraph.
- [**How to implement multi-turn conversations**](../how-tos/multi-agent-multi-turn-convo.ipynb): Learn how to implement multi-turn conversations in LangGraph.
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+2
View File
@@ -24,7 +24,9 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [LangGraph Glossary](low_level.md): LangGraph workflows are designed as graphs, with nodes representing different components and edges representing the flow of information between them. This guide provides an overview of the key concepts associated with LangGraph graph primitives.
- [Common Agentic Patterns](agentic_concepts.md): An agent uses an LLM to pick its own control flow to solve more complex problems! Agents are a key building block in many LLM applications. This guide explains the different types of agent architectures and how they can be used to control the flow of an application.
- [Multi-Agent Systems](multi_agent.md): Complex LLM applications can often be broken down into multiple agents, each responsible for a different part of the application. This guide explains common patterns for building multi-agent systems.
- [Breakpoints](breakpoints.md): Breakpoints allow pausing the execution of a graph at specific points. Breakpoints allow stepping through graph execution for debugging purposes.
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
+18 -1
View File
@@ -21,6 +21,18 @@ See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for c
| Development | 1 CPU | 1 GB | Up to 1 container |
| Production | 1 CPU | 2 GB | Up to 10 containers |
## Autoscaling
`Production` type deployments automatically scale up to 10 containers. Scaling is based on the current request load for a single container. Specifically, the autoscaling implementation scales the deployment so that each container is processing about 10 concurrent requests. For example...
- If the deployment is processing 20 concurrent requests, the deployment will scale up from 1 container to 2 containers (20 requests / 2 containers = 10 requests per container).
- If a deployment of 2 containers is processing 10 requests, the deployment will scale down from 2 containers to 1 container (10 requests / 1 container = 10 requests per container).
10 concurrent requests per container is the target threshold. However, 10 concurrent requests per container is not a hard limit. The number of concurrent requests can exceed 10 if there is a sudden burst of requests.
Scale down actions are delayed for 30 minutes before any action is taken. In other words, if the autoscaling implementation decides to scale down a deployment, it will first wait for 30 minutes before scaling down. After 30 minutes, the concurrency metric is recomputed and the deployment will scale down if the concurrency metric has met the target threshold. Otherwise, the deployment remains scaled up. This "cool down" period ensures that deployments do not scale up and down too frequently.
In the future, the autoscaling implementation may evolve to accommodate other metrics such as background run queue size.
## Revision
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
@@ -31,6 +43,12 @@ See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for cre
Infrastructure for [deployments](#deployment) and [revisions](#revision) are provisioned and deployed asynchronously. They are not deployed immediately after submission. Currently, deployment can take up to several minutes.
- When a new deployment is created, a new database is created for the deployment. Database creation is a one-time step. This step contributes to a longer deployment time for the initial revision of the deployment.
- When a subsequent revision is created for a deployment, there is no database creation step. The deployment time for a subsequent revision is significantly faster compared to the deployment time of the initial revision.
- The deployment process for each revision contains a build step, which can take up to a few minutes.
!!! info "Database creation for `Development` type deployments takes longer than database creation for `Production` type deployments."
## Architecture
!!! warning "Subject to Change"
@@ -40,7 +58,6 @@ A high-level diagram of a Cloud SaaS deployment.
![diagram](img/langgraph_cloud_architecture.png)
## Related
- [Deployment Options](./deployment_options.md)
+59 -59
View File
@@ -339,37 +339,6 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
)
```
`Command` has the following properties:
| Property | Description |
| --- | --- |
| `graph` | Graph to send the command to. Supported values:<br>- `None`: the current graph (default)<br>- `Command.PARENT`: closest parent graph |
| `update` | Update to apply to the graph's state. |
| `resume` | Value to resume execution with. To be used together with [`interrupt()`][langgraph.types.interrupt]. |
| `goto` | Can be one of the following:<br>- name of the node to navigate to next (any node that belongs to the specified `graph`)<br>- sequence of node names to navigate to next<br>- `Send` object (to execute a node with the input provided)<br>- sequence of `Send` objects<br>If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
```python
from langgraph.graph import StateGraph, START
from langgraph.types import Command
from typing_extensions import Literal, TypedDict
class State(TypedDict):
foo: str
def my_node(state: State) -> Command[Literal["my_other_node"]]:
return Command(update={"foo": "bar"}, goto="my_other_node")
def my_other_node(state: State):
return {"foo": state["foo"] + "baz"}
builder = StateGraph(State)
builder.add_edge(START, "my_node")
builder.add_node("my_node", my_node)
builder.add_node("my_other_node", my_other_node)
graph = builder.compile()
```
With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
```python
@@ -380,10 +349,44 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
!!! important
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["node_b", "node_c"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`.
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["my_other_node"]]`. This is necessary for the graph rendering and tells LangGraph that `my_node` can navigate to `my_other_node`.
Check out this [how-to guide](../how-tos/command.ipynb) for an end-to-end example of how to use `Command`.
### When should I use Command instead of conditional edges?
Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](./multi_agent.md#handoffs) where it's important to route to a different agent and pass some information to that agent.
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
### Using inside tools
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool:
```python
@tool
def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):
"""Use this to look up user information to better assist them with their questions."""
user_info = get_user_info(config.get("configurable", {}).get("user_id"))
return Command(
update={
# update the state keys
"user_info": user_info,
# update the message history
"messages": [ToolMessage("Successfully looked up user information", tool_call_id=tool_call_id)]
}
)
```
!!! important
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
### Human-in-the-loop
`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `Command(resume="User input")`. Check out [this conceptual guide](./human_in_the_loop.md) for more information.
## Persistence
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
@@ -449,35 +452,32 @@ graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthr
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
## `interrupt`
Use the [interrupt](../reference/types.md/#langgraph.types.interrupt) function to **pause** the graph at specific points to collect user input. The `interrupt` function surfaces interrupt information to the client, allowing the developer to collect user input, validate the graph state, or make decisions before resuming execution.
```python
from langgraph.types import interrupt
def human_approval_node(state: State):
...
answer = interrupt(
# This value will be sent to the client.
# It can be any JSON serializable value.
{"question": "is it ok to continue?"},
)
...
```
Resuming the graph is done by passing a [`Command`](#command) object to the graph with the `resume` key set to the value returned by the `interrupt` function.
Read more about how the `interrupt` is used for **human-in-the-loop** workflows in the [Human-in-the-loop conceptual guide](./human_in_the_loop.md).
## Breakpoints
It can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you ["compile" a graph](#compiling-your-graph). You can set breakpoints either _before_ a node executes (using `interrupt_before`) or after a node executes (using `interrupt_after`.)
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
You **MUST** use a [checkpointer](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
In order to resume execution, you can just invoke your graph with `None` as the input.
```python
# Initial run of graph
graph.invoke(inputs, config=config)
# Let's assume it hit a breakpoint somewhere, you can then resume by passing in None
graph.invoke(None, config=config)
```
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a full walkthrough of how to add breakpoints.
### Dynamic Breakpoints
It may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. In `LangGraph` you can do so by using `NodeInterrupt` -- a special exception that can be raised from inside a node.
```python
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
```
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
## Subgraphs
@@ -518,7 +518,7 @@ The simplest way to create subgraph nodes is by using a [compiled subgraph](#com
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
```python
from langgraph.graph import START, StateGraph
from langgraph.graph import StateGraph
from typing import TypedDict
class State(TypedDict):
+153 -51
View File
@@ -26,13 +26,88 @@ There are several ways to connect agents in a multi-agent system:
- **Hierarchical**: you can define a multi-agent system with [a supervisor of supervisors](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/). This is a generalization of the supervisor architecture and allows for more complex control flows.
- **Custom multi-agent workflow**: each agent communicates with only a subset of agents. Parts of the flow are deterministic, and only some agents can decide which other agents to call next.
### Handoffs
In multi-agent architectures, agents can be represented as graph nodes. Each agent node executes its step(s) and decides whether to finish execution or route to another agent, including potentially routing to itself (e.g., running in a loop). A common pattern in multi-agent interactions is handoffs, where one agent hands off control to another. Handoffs allow you to specify:
- __destination__: target agent to navigate to (e.g., name of the node to go to)
- __payload__: [information to pass to that agent](#communication-between-agents) (e.g., state update)
To implement handoffs in LangGraph, agent nodes can return [`Command`](./low_level.md#command) object that allows you to combine both control flow and state updates:
```python
def agent(state) -> Command[Literal["agent", "another_agent"]]:
# the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
goto = get_next_agent(...) # 'agent' / 'another_agent'
return Command(
# Specify which agent to call next
goto=goto,
# Update the graph state
update={"my_state_key": "my_state_value"}
)
```
In a more complex scenario where each agent node is itself a graph (i.e., a [subgraph](./low_level.md#subgraphs)), a node in one of the agent subgraphs might want to navigate to a different agent. For example, if you have two agents, `alice` and `bob` (subgraph nodes in a parent graph), and `alice` needs to navigate to `bob`, you can set `graph=Command.PARENT` in the `Command` object:
```python
def some_node_inside_alice(state)
return Command(
goto="bob",
update={"my_state_key": "my_state_value"},
# specify which graph to navigate to (defaults to the current graph)
graph=Command.PARENT,
)
```
!!! note
If you need to support visualization for subgraphs communicating using `Command(graph=Command.PARENT)` you would need to wrap them in a node function with `Command` annotation, e.g. instead of this:
```python
builder.add_node(alice)
```
you would need to do this:
```python
def call_alice(state) -> Command[Literal["bob"]]:
return alice.invoke(state)
builder.add_node("alice", call_alice)
```
#### Handoffs as tools
One of the most common agent types is a ReAct-style tool-calling agents. For those types of agents, a common pattern is wrapping a handoff in a tool call, e.g.:
```python
def transfer_to_bob(state):
"""Transfer to bob."""
return Command(
goto="bob",
update={"my_state_key": "my_state_value"},
graph=Command.PARENT,
)
```
This is a special case of updating the graph state from tools where in addition the state update, the control flow is included as well.
!!! important
If you want to use tools that return `Command`, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:
```python
def call_tools(state):
...
commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls]
return commands
```
Let's now take a closer look at the different multi-agent architectures.
### Network
In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. This architecture is good for problems that do not have a clear hierarchy of agents or a specific sequence in which agents should be called.
### Supervisor
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [conditional edges](./low_level.md#conditional-edges) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
```python
from typing import Literal
@@ -41,39 +116,83 @@ from langgraph.graph import StateGraph, MessagesState, START
model = ChatOpenAI()
class AgentState(MessagesState):
next: Literal["agent_1", "agent_2", "__end__"]
def supervisor(state: AgentState):
def agent_1(state: MessagesState) -> Command[Literal["agent_2", "agent_3", END]]:
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# to determine which agent to call next. a common pattern is to call the model
# with a structured output (e.g. force it to return an output with a "next_agent" field)
response = model.invoke(...)
# the "next" key will be used by the conditional edges to route execution
# to the appropriate agent
return {"next": response["next_agent"]}
# route to one of the agents or exit based on the LLM's decision
# if the LLM returns "__end__", the graph will finish execution
return Command(
goto=response["next_agent"],
update={"messages": [response["content"]]},
)
def agent_1(state: AgentState):
def agent_2(state: MessagesState) -> Command[Literal["agent_1", "agent_3", END]]:
response = model.invoke(...)
return Command(
goto=response["next_agent"],
update={"messages": [response["content"]]},
)
def agent_3(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
...
return Command(
goto=response["next_agent"],
update={"messages": [response["content"]]},
)
builder = StateGraph(MessagesState)
builder.add_node(agent_1)
builder.add_node(agent_2)
builder.add_node(agent_3)
builder.add_edge(START, "agent_1")
network = builder.compile()
```
### Supervisor
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](./low_level.md#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
```python
from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI()
def supervisor(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# to determine which agent to call next. a common pattern is to call the model
# with a structured output (e.g. force it to return an output with a "next_agent" field)
response = model.invoke(...)
# route to one of the agents or exit based on the supervisor's decision
# if the supervisor returns "__end__", the graph will finish execution
return Command(goto=response["next_agent"])
def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]:
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# and add any additional logic (different models, custom prompts, structured output, etc.)
response = model.invoke(...)
return {"messages": [response]}
return Command(
goto="supervisor",
update={"messages": [response]},
)
def agent_2(state: AgentState):
def agent_2(state: MessagesState) -> Command[Literal["supervisor"]]:
response = model.invoke(...)
return {"messages": [response]}
return Command(
goto="supervisor",
update={"messages": [response]},
)
builder = StateGraph(AgentState)
builder = StateGraph(MessagesState)
builder.add_node(supervisor)
builder.add_node(agent_1)
builder.add_node(agent_2)
builder.add_edge(START, "supervisor")
# route to one of the agents or exit based on the supervisor's decisiion
# if the supervisor returns "__end__", the graph will finish execution
builder.add_conditional_edges("supervisor", lambda state: state["next"])
builder.add_edge("agent_1", "supervisor")
builder.add_edge("agent_2", "supervisor")
supervisor = builder.compile()
```
@@ -121,37 +240,29 @@ To address this, you can design your system _hierarchically_. For example, you c
```python
from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI()
# define team 1 (same as the single supervisor example above)
class Team1State(MessagesState):
next: Literal["team_1_agent_1", "team_1_agent_2", "__end__"]
def team_1_supervisor(state: Team1State):
def team_1_supervisor(state: MessagesState) -> Command[Literal["team_1_agent_1", "team_1_agent_2", END]]:
response = model.invoke(...)
return {"next": response["next_agent"]}
return Command(goto=response["next_agent"])
def team_1_agent_1(state: Team1State):
def team_1_agent_1(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
response = model.invoke(...)
return {"messages": [response]}
return Command(goto="team_1_supervisor", update={"messages": [response]})
def team_1_agent_2(state: Team1State):
def team_1_agent_2(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
response = model.invoke(...)
return {"messages": [response]}
return Command(goto="team_1_supervisor", update={"messages": [response]})
team_1_builder = StateGraph(Team1State)
team_1_builder.add_node(team_1_supervisor)
team_1_builder.add_node(team_1_agent_1)
team_1_builder.add_node(team_1_agent_2)
team_1_builder.add_edge(START, "team_1_supervisor")
# route to one of the agents or exit based on the supervisor's decisiion
# if the supervisor returns "__end__", the graph will finish execution
team_1_builder.add_conditional_edges("team_1_supervisor", lambda state: state["next"])
team_1_builder.add_edge("team_1_agent_1", "team_1_supervisor")
team_1_builder.add_edge("team_1_agent_2", "team_1_supervisor")
team_1_graph = team_1_builder.compile()
# define team 2 (same as the single supervisor example above)
@@ -174,31 +285,22 @@ team_2_graph = team_2_builder.compile()
# define top-level supervisor
class TopLevelState(MessagesState):
next: Literal["team_1", "team_2", "__end__"]
builder = StateGraph(TopLevelState)
def top_level_supervisor(state: TopLevelState):
builder = StateGraph(MessagesState)
def top_level_supervisor(state: MessagesState):
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# to determine which team to call next. a common pattern is to call the model
# with a structured output (e.g. force it to return an output with a "next_team" field)
response = model.invoke(...)
# the "next" key will be used by the conditional edges to route execution
# to the appropriate team
return {"next": response["next_team"]}
# route to one of the teams or exit based on the supervisor's decision
# if the supervisor returns "__end__", the graph will finish execution
return Command(goto=response["next_team"])
builder = StateGraph(TopLevelState)
builder = StateGraph(MessagesState)
builder.add_node(top_level_supervisor)
builder.add_node(team_1_graph)
builder.add_node(team_2_graph)
builder.add_edge(START, "top_level_supervisor")
# route to one of the teams or exit based on the supervisor's decision
# if the top-level supervisor returns "__end__", the graph will finish execution
builder.add_conditional_edges("top_level_supervisor", lambda state: state["next"])
builder.add_edge("team_1_graph", "top_level_supervisor")
builder.add_edge("team_2_graph", "top_level_supervisor")
graph = builder.compile()
```
@@ -208,7 +310,7 @@ In this architecture we add individual agents as graph nodes and define the orde
- **Explicit control flow (normal edges)**: LangGraph allows you to explicitly define the control flow of your application (i.e. the sequence of how agents communicate) explicitly, via [normal graph edges](./low_level.md#normal-edges). This is the most deterministic variant of this architecture above — we always know which agent will be called next ahead of time.
- **Dynamic control flow (conditional edges)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [conditional edges](./low_level.md#conditional-edges). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called.
- **Dynamic control flow (Command)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [`Command`](./low_level.md#command). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called.
```python
from langchain_openai import ChatOpenAI
+3 -3
View File
@@ -168,7 +168,7 @@ Importantly, LangGraph knows whether a particular checkpoint has been executed p
### Update state
In addition to re-playing the graph from specific `checkpoints`, we can also *edit* the graph state. We do this using `graph.update_state()`. This method three different arguments:
In addition to re-playing the graph from specific `checkpoints`, we can also *edit* the graph state. We do this using `graph.update_state()`. This method accepts three different arguments:
#### `config`
@@ -222,7 +222,7 @@ The final thing you can optionally specify when calling `update_state` is `as_no
A [state schema](low_level.md#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence.
But, what if we want to retrain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
But, what if we want to retain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](../reference/store.md#langgraph.store.base.BaseStore) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
@@ -471,7 +471,7 @@ Second, checkpointers allow for ["memory"](agentic_concepts.md#memory) between i
### Time Travel
Third, checkpointers allow for ["time travel"](../how-tos/human_in_the_loop/time-travel.ipynb), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories.
Third, checkpointers allow for ["time travel"](time-travel.md), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories.
### Fault-tolerance
+72
View File
@@ -0,0 +1,72 @@
# Time Travel ⏱️
!!! note "Prerequisites"
This guide assumes that you are familiar with LangGraph's checkpoints and states. If not, please review the [persistence](./persistence.md) concept first.
When working with non-deterministic systems that make model-based decisions (e.g., agents powered by LLMs), it can be useful to examine their decision-making process in detail:
1. 🤔 **Understand Reasoning**: Analyze the steps that led to a successful result.
2. 🐞 **Debug Mistakes**: Identify where and why errors occurred.
3. 🔍 **Explore Alternatives**: Test different paths to uncover better solutions.
We call these debugging techniques **Time Travel**, composed of two key actions: [**Replaying**](#replaying) 🔁 and [**Forking**](#forking) 🔀 .
## Replaying
![](./img/human_in_the_loop/replay.png)
Replaying allows us to revisit and reproduce an agent's past actions. This can be done either from the current state (or checkpoint) of the graph or from a specific checkpoint.
To replay from the current state, simply pass `None` as the input along with a `thread`:
```python
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
To replay actions from a specific checkpoint, start by retrieving all checkpoints for the thread:
```python
all_checkpoints = []
for state in graph.get_state_history(thread):
all_checkpoints.append(state)
```
Each checkpoint has a unique ID. After identifying the desired checkpoint, for instance, `xyz`, include its ID in the configuration:
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xyz'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
The graph efficiently replays previously executed nodes instead of re-executing them, leveraging its awareness of prior checkpoint executions.
## Forking
![](./img/human_in_the_loop/forking.png)
Forking allows you to revisit an agent's past actions and explore alternative paths within the graph.
To edit a specific checkpoint, such as `xyz`, provide its `checkpoint_id` when updating the graph's state:
```python
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xyz"}}
graph.update_state(config, {"state": "updated state"})
```
This creates a new forked checkpoint, xyz-fork, from which you can continue running the graph:
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xyz-fork'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
## Additional Resources 📚
- [**Conceptual Guide: Persistence**](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay): Read the persistence guide for more context on replaying.
- [**How to View and Update Past Graph State**](../how-tos/human_in_the_loop/time-travel.ipynb): Step-by-step instructions for working with graph state that demonstrate the **replay** and **fork** actions.
+329
View File
@@ -0,0 +1,329 @@
# Human-in-the-loop
!!! note "Use the `interrupt` function instead."
As of LangGraph 0.2.57, the recommended way to set breakpoints is using the [`interrupt` function][langgraph.types.interrupt] as it simplifies **human-in-the-loop** patterns.
Please see the revised [human-in-the-loop guide](./human_in_the_loop.md) for the latest version that uses the `interrupt` function.
Human-in-the-loop (or "on-the-loop") enhances agent capabilities through several common user interaction patterns.
Common interaction patterns include:
(1) `Approval` - We can interrupt our agent, surface the current state to a user, and allow the user to accept an action.
(2) `Editing` - We can interrupt our agent, surface the current state to a user, and allow the user to edit the agent state.
(3) `Input` - We can explicitly create a graph node to collect human input and pass that input directly to the agent state.
Use-cases for these interaction patterns include:
(1) `Reviewing tool calls` - We can interrupt an agent to review and edit the results of tool calls.
(2) `Time Travel` - We can manually re-play and / or fork past actions of an agent.
## Persistence
All of these interaction patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which will write a checkpoint of the graph state at each step. Persistence allows the graph to stop so that a human can review and / or edit the current state of the graph and then resume with the human's input.
### Breakpoints
Adding a [breakpoint](./breakpoints.md) a specific location in the graph flow is one way to enable human-in-the-loop. In this case, the developer knows *where* in the workflow human input is needed and simply places a breakpoint prior to or following that particular graph node.
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
```python
# Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["step_for_human_in_the_loop"])
# Run the graph up to the breakpoint
thread_config = {"configurable": {"thread_id": "1"}}
for event in graph.stream(inputs, thread_config, stream_mode="values"):
print(event)
# Perform some action that requires human in the loop
# Continue the graph execution from the current checkpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
### Dynamic Breakpoints
Alternatively, the developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./breakpoints.md) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
```python
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
```
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
```python
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
The graph will *interrupt* again because this node will be *re-run* with the same graph state. We need to change the graph state such that the condition that triggers the dynamic breakpoint is no longer met. So, we can simply edit the graph state to an input that meets the condition of our dynamic breakpoint (< 5 characters) and re-run the node.
```python
# Update the state to pass the dynamic breakpoint
graph.update_state(config=thread_config, values={"input": "foo"})
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
Alternatively, what if we want to keep our current input and skip the node (`my_node`) that performs the check? To do this, we can simply perform the graph update with `as_node="my_node"` and pass in `None` for the values. This will make no update the graph state, but run the update as `my_node`, effectively skipping the node and bypassing the dynamic breakpoint.
```python
# This update will skip the node `my_node` altogether
graph.update_state(config=thread_config, values=None, as_node="my_node")
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
See [our guide](../how-tos/human_in_the_loop/dynamic_breakpoints.ipynb) for a detailed how-to on doing this!
## Interaction Patterns
### Approval
![](./img/human_in_the_loop/approval.png)
Sometimes we want to approve certain steps in our agent's execution.
We can interrupt our agent at a [breakpoint](./breakpoints.md) prior to the step that we want to approve.
This is generally recommend for sensitive actions (e.g., using external APIs or writing to a database).
With persistence, we can surface the current agent state as well as the next step to a user for review and approval.
If approved, the graph resumes execution from the last saved checkpoint, which is saved to the `thread`:
```python
# Compile our graph with a checkpointer and a breakpoint before the step to approve
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# ... Get human approval ...
# If approved, continue the graph execution from the last saved checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed how-to on doing this!
### Editing
![](./img/human_in_the_loop/edit_graph_state.png)
Sometimes we want to review and edit the agent's state.
As with approval, we can interrupt our agent at a [breakpoint](./breakpoints.md) prior to the step we want to check.
We can surface the current state to a user and allow the user to edit the agent state.
This can, for example, be used to correct the agent if it made a mistake (e.g., see the section on tool calling below).
We can edit the graph state by forking the current checkpoint, which is saved to the `thread`.
We can then proceed with the graph from our forked checkpoint as done before.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to review
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the state, decide to edit it, and create a forked checkpoint with the new state
graph.update_state(thread, {"state": "new state"})
# Continue the graph execution from the forked checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
See [this guide](../how-tos/human_in_the_loop/edit-graph-state.ipynb) for a detailed how-to on doing this!
### Input
![](./img/human_in_the_loop/wait_for_input.png)
Sometimes we want to explicitly get human input at a particular step in the graph.
We can create a graph node designated for this (e.g., `human_input` in our example diagram).
As with approval and editing, we can interrupt our agent at a [breakpoint](./breakpoints.md) prior to this node.
We can then perform a state update that includes the human input, just as we did with editing state.
But, we add one thing:
We can use `as_node=human_input` with the state update to specify that the state update *should be treated as a node*.
The is subtle, but important:
With editing, the user makes a decision about whether or not to edit the graph state.
With input, we explicitly define a node in our graph for collecting human input!
The state update with the human input then runs *as this node*.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to to collect human input
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_input"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Update the state with the user input as if it was the human_input node
graph.update_state(thread, {"user_input": user_input}, as_node="human_input")
# Continue the graph execution from the checkpoint created by the human_input node
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
See [this guide](../how-tos/human_in_the_loop/wait-user-input.ipynb) for a detailed how-to on doing this!
## Use-cases
### Reviewing Tool Calls
Some user interaction patterns combine the above ideas.
For example, many agents use [tool calling](https://python.langchain.com/docs/how_to/tool_calling/) to make decisions.
Tool calling presents a challenge because the agent must get two things right:
(1) The name of the tool to call
(2) The arguments to pass to the tool
Even if the tool call is correct, we may also want to apply discretion:
(3) The tool call may be a sensitive operation that we want to approve
With these points in mind, we can combine the above ideas to create a human-in-the-loop review of a tool call.
```python
# Compile our graph with a checkpointer and a breakpoint before the step to to review the tool call from the LLM
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_review"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the tool call and update it, if needed, as the human_review node
graph.update_state(thread, {"tool_call": "updated tool call"}, as_node="human_review")
# Otherwise, approve the tool call and proceed with the graph execution with no edits
# Continue the graph execution from either:
# (1) the forked checkpoint created by human_review or
# (2) the checkpoint saved when the tool call was originally made (no edits in human_review)
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for a detailed how-to on doing this!
### Time Travel
When working with agents, we often want closely examine their decision making process:
(1) Even when they arrive a desired final result, the reasoning that led to that result is often important to examine.
(2) When agents make mistakes, it is often valuable to understand why.
(3) In either of the above cases, it is useful to manually explore alternative decision making paths.
Collectively, we call these debugging concepts `time-travel` and they are composed of `replaying` and `forking`.
#### Replaying
![](./img/human_in_the_loop/replay.png)
Sometimes we want to simply replay past actions of an agent.
Above, we showed the case of executing an agent from the current state (or checkpoint) of the graph.
We by simply passing in `None` for the input with a `thread`.
```
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
Now, we can modify this to replay past actions from a *specific* checkpoint by passing in the checkpoint ID.
To get a specific checkpoint ID, we can easily get all of the checkpoints in the thread and filter to the one we want.
```python
all_checkpoints = []
for state in app.get_state_history(thread):
all_checkpoints.append(state)
```
Each checkpoint has a unique ID, which we can use to replay from a specific checkpoint.
Assume from reviewing the checkpoints that we want to replay from one, `xxx`.
We just pass in the checkpoint ID when we run the graph.
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
Importantly, the graph knows which checkpoints have been previously executed.
So, it will re-play any previously executed nodes rather than re-executing them.
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#replay) for related context on replaying.
See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
#### Forking
![](./img/human_in_the_loop/forking.png)
Sometimes we want to fork past actions of an agent, and explore different paths through the graph.
`Editing`, as discussed above, is *exactly* how we do this for the *current* state of the graph!
But, what if we want to fork *past* states of the graph?
For example, let's say we want to edit a particular checkpoint, `xxx`.
We pass this `checkpoint_id` when we update the state of the graph.
```python
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xxx"}}
graph.update_state(config, {"state": "updated state"}, )
```
This creates a new forked checkpoint, `xxx-fork`, which we can then run the graph from.
```python
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx-fork'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
```
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state) for related context on forking.
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
+3 -3
View File
@@ -25,7 +25,7 @@
"\n",
"```python\n",
"def my_node(state: State) -> Command[Literal[\"my_other_node\"]]:\n",
" return GraphCommand(\n",
" return Command(\n",
" # state update\n",
" update={\"foo\": \"bar\"},\n",
" # control flow\n",
@@ -144,7 +144,7 @@
"id": "badc25eb-4876-482e-bb10-d763023cdaad",
"metadata": {},
"source": [
"We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../../concepts/low_level#conditional-edges) for routing! This is because control flow is defined with `GraphCommand` inside `node_a`."
"We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../../concepts/low_level#conditional-edges) for routing! This is because control flow is defined with `Command` inside `node_a`."
]
},
{
@@ -171,7 +171,7 @@
"source": [
"!!! important\n",
"\n",
" You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
" You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph rendering and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
]
},
{
@@ -12,6 +12,14 @@
"source": [
"# How to add breakpoints\n",
"\n",
"!!! tip \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following concepts:\n",
"\n",
" * [Breakpoints](../../../concepts/breakpoints)\n",
" * [LangGraph Glossary](../../../concepts/low_level)\n",
" \n",
"\n",
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions). \n",
"\n",
"Breakpoints are built on top of LangGraph [checkpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer), which save the graph's state after each node execution. Checkpoints are saved in [threads](https://langchain-ai.github.io/langgraph/concepts/low_level/#threads) that preserve graph state and can be accessed after a graph has finished execution. This allows for graph execution to pause at specific points, await human approval, and then resume execution from the last checkpoint.\n",
@@ -467,7 +475,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.8"
"version": "3.11.4"
}
},
"nbformat": 4,
@@ -1,24 +1,32 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "ee54cde3-7e4d-43f4-b921-e7141ea0f19e",
"metadata": {},
"source": [
"# How to add dynamic breakpoints"
]
},
{
"cell_type": "markdown",
"id": "607849c6-4b8c-4e06-ad9c-758bb5a08e86",
"id": "b7d5f6a5-9e59-43e4-a4b6-8ada6dace691",
"metadata": {},
"source": [
"# How to add dynamic breakpoints with `NodeInterrupt`\n",
"\n",
"!!! note\n",
"\n",
" For **human-in-the-loop** workflows use the new [`interrupt()`](../../../reference/types/#langgraph.types.interrupt) function for **human-in-the-loop** workflows. Please review the [Human-in-the-loop conceptual guide](../../../concepts/human_in_the_loop) for more information about design patterns with `interrupt`.\n",
"\n",
"!!! tip \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following concepts:\n",
"\n",
" * [Breakpoints](../../../concepts/breakpoints)\n",
" * [LangGraph Glossary](../../../concepts/low_level)\n",
" \n",
"\n",
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).\n",
"\n",
"In LangGraph you can add breakpoints before / after a node is executed. But oftentimes it may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. When doing so, it may also be helpful to include information about **why** that interrupt was raised.\n",
"\n",
"This guide shows how you can dynamically interrupt the graph using `NodeInterrupt` -- a special exception that can be raised from inside a node. Let's see it in action!\n",
"\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages"
@@ -430,7 +438,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.11.4"
}
},
"nbformat": 4,
@@ -12,6 +12,12 @@
"source": [
"# How to edit graph state\n",
"\n",
"!!! tip \"Prerequisites\"\n",
"\n",
" * [Human-in-the-loop](../../../concepts/human_in_the_loop)\n",
" * [Breakpoints](../../../concepts/breakpoints)\n",
" * [LangGraph Glossary](../../../concepts/low_level)\n",
"\n",
"Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). Manually updating the graph state a common HIL interaction pattern, allowing the human to edit actions (e.g., what tool is being called or how it is being called).\n",
"\n",
"We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state and then resume from that spot to continue. \n",
@@ -554,7 +560,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.8"
"version": "3.11.4"
}
},
"nbformat": 4,
File diff suppressed because one or more lines are too long
@@ -7,6 +7,15 @@
"source": [
"# How to view and update past graph state\n",
"\n",
"!!! tip \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following concepts:\n",
"\n",
" * [Time Travel](../../../concepts/time-travel)\n",
" * [Breakpoints](../../../concepts/breakpoints)\n",
" * [LangGraph Glossary](../../../concepts/low_level)\n",
"\n",
"\n",
"Once you start [checkpointing](../../persistence) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n",
"\n",
"1. You can surface a state during an interrupt to a user to let them accept an action.\n",
@@ -589,7 +598,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.11.4"
}
},
"nbformat": 4,
File diff suppressed because one or more lines are too long
+29 -6
View File
@@ -30,7 +30,7 @@ These how-to guides show how to achieve that controllability.
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
- [How to add cross-thread persistence to your graph](cross-thread-persistence.ipynb)
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
- [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb)
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
### Memory
@@ -48,12 +48,24 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb)
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb)
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb)
Key workflows:
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb): A basic example that shows how to implement a human-in-the-loop workflow in your graph using the `interrupt` function.
- [How to review tool calls](human_in_the_loop/review-tool-calls.ipynb): Incorporate human-in-the-loop for reviewing/editing/accepting tool call requests before they executed using the `interrupt` function.
Other methods:
- [How to add static breakpoints](human_in_the_loop/breakpoints.ipynb): Use for debugging purposes. For [**human-in-the-loop**](../concepts/human_in_the_loop.md) workflows, we recommend the [`interrupt` function][langgraph.types.interrupt] instead.
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
### Time Travel
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb)
- [How to review tool calls](human_in_the_loop/review-tool-calls.ipynb)
### Streaming
@@ -81,6 +93,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to handle tool calling errors](tool-calling-errors.ipynb)
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
- [How to pass config to tools](pass-config-to-tools.ipynb)
- [How to update graph state from tools](update-state-from-tools.ipynb)
- [How to handle large numbers of tools](many-tools.ipynb)
### Subgraphs
@@ -91,6 +104,15 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
### Multi-agent
[Multi-agent systems](../concepts/multi_agent.md) are useful to break down complex LLM applications into multiple agents, each responsible for a different part of the application. These how-to guides show how to implement multi-agent systems in LangGraph:
- [How to build a multi-agent network](multi-agent-network.ipynb)
- [How to add multi-turn conversation in a multi-agent application](multi-agent-multi-turn-convo.ipynb)
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
### State Management
- [How to use Pydantic model as state](state-model.ipynb)
@@ -225,6 +247,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
- [How to connect to a local deployment (Docker)](../cloud/how-tos/test_local_deployment.md)
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
## Troubleshooting
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 it is too large Load Diff
+100 -55
View File
@@ -151,6 +151,7 @@
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"from langgraph.checkpoint.base import (\n",
" WRITES_IDX_MAP,\n",
" BaseCheckpointSaver,\n",
" ChannelVersions,\n",
" Checkpoint,\n",
@@ -163,7 +164,7 @@
"from redis import Redis\n",
"from redis.asyncio import Redis as AsyncRedis\n",
"\n",
"REDIS_KEY_SEPARATOR = \":\"\n",
"REDIS_KEY_SEPARATOR = \"$\"\n",
"\n",
"\n",
"# Utilities shared by both RedisSaver and AsyncRedisSaver\n",
@@ -246,17 +247,6 @@
" return keys\n",
"\n",
"\n",
"def _dump_writes(serde: SerializerProtocol, writes: tuple[str, Any]) -> list[dict]:\n",
" \"\"\"Serialize pending writes.\"\"\"\n",
" serialized_writes = []\n",
" for channel, value in writes:\n",
" type_, serialized_value = serde.dumps_typed(value)\n",
" serialized_writes.append(\n",
" {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
" )\n",
" return serialized_writes\n",
"\n",
"\n",
"def _load_writes(\n",
" serde: SerializerProtocol, task_id_to_data: dict[tuple[str, str], dict]\n",
") -> list[PendingWrite]:\n",
@@ -413,7 +403,7 @@
" config: RunnableConfig,\n",
" writes: List[Tuple[str, Any]],\n",
" task_id: str,\n",
" ) -> RunnableConfig:\n",
" ) -> None:\n",
" \"\"\"Store intermediate writes linked to a checkpoint.\n",
"\n",
" Args:\n",
@@ -425,12 +415,23 @@
" checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
" checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
"\n",
" for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
" for idx, (channel, value) in enumerate(writes):\n",
" key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
" thread_id,\n",
" checkpoint_ns,\n",
" checkpoint_id,\n",
" task_id,\n",
" WRITES_IDX_MAP.get(channel, idx),\n",
" )\n",
" self.conn.hset(key, mapping=data)\n",
" return config\n",
" type_, serialized_value = self.serde.dumps_typed(value)\n",
" data = {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
" if all(w[0] in WRITES_IDX_MAP for w in writes):\n",
" # Use HSET which will overwrite existing values\n",
" self.conn.hset(key, mapping=data)\n",
" else:\n",
" # Use HSETNX which will not overwrite existing values\n",
" for field, value in data.items():\n",
" self.conn.hsetnx(key, field, value)\n",
"\n",
" def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
" \"\"\"Get a checkpoint tuple from Redis.\n",
@@ -463,21 +464,8 @@
" checkpoint_id\n",
" or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
" )\n",
" writes_key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
" )\n",
" matching_keys = self.conn.keys(pattern=writes_key)\n",
" parsed_keys = [\n",
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
" ]\n",
" pending_writes = _load_writes(\n",
" self.serde,\n",
" {\n",
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): self.conn.hgetall(key)\n",
" for key, parsed_key in sorted(\n",
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
" )\n",
" },\n",
" pending_writes = self._load_pending_writes(\n",
" thread_id, checkpoint_ns, checkpoint_id\n",
" )\n",
" return _parse_redis_checkpoint_data(\n",
" self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
@@ -514,7 +502,37 @@
" for key in keys:\n",
" data = self.conn.hgetall(key)\n",
" if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
" yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
" # load pending writes\n",
" checkpoint_id = _parse_redis_checkpoint_key(key.decode())[\n",
" \"checkpoint_id\"\n",
" ]\n",
" pending_writes = self._load_pending_writes(\n",
" thread_id, checkpoint_ns, checkpoint_id\n",
" )\n",
" yield _parse_redis_checkpoint_data(\n",
" self.serde, key.decode(), data, pending_writes=pending_writes\n",
" )\n",
"\n",
" def _load_pending_writes(\n",
" self, thread_id: str, checkpoint_ns: str, checkpoint_id: str\n",
" ) -> List[PendingWrite]:\n",
" writes_key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
" )\n",
" matching_keys = self.conn.keys(pattern=writes_key)\n",
" parsed_keys = [\n",
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
" ]\n",
" pending_writes = _load_writes(\n",
" self.serde,\n",
" {\n",
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): self.conn.hgetall(key)\n",
" for key, parsed_key in sorted(\n",
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
" )\n",
" },\n",
" )\n",
" return pending_writes\n",
"\n",
" def _get_checkpoint_key(\n",
" self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
@@ -637,7 +655,7 @@
" config: RunnableConfig,\n",
" writes: List[Tuple[str, Any]],\n",
" task_id: str,\n",
" ) -> RunnableConfig:\n",
" ) -> None:\n",
" \"\"\"Store intermediate writes linked to a checkpoint asynchronously.\n",
"\n",
" This method saves intermediate writes associated with a checkpoint to the database.\n",
@@ -651,12 +669,23 @@
" checkpoint_ns = config[\"configurable\"][\"checkpoint_ns\"]\n",
" checkpoint_id = config[\"configurable\"][\"checkpoint_id\"]\n",
"\n",
" for idx, data in enumerate(_dump_writes(self.serde, writes)):\n",
" for idx, (channel, value) in enumerate(writes):\n",
" key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, task_id, idx\n",
" thread_id,\n",
" checkpoint_ns,\n",
" checkpoint_id,\n",
" task_id,\n",
" WRITES_IDX_MAP.get(channel, idx),\n",
" )\n",
" await self.conn.hset(key, mapping=data)\n",
" return config\n",
" type_, serialized_value = self.serde.dumps_typed(value)\n",
" data = {\"channel\": channel, \"type\": type_, \"value\": serialized_value}\n",
" if all(w[0] in WRITES_IDX_MAP for w in writes):\n",
" # Use HSET which will overwrite existing values\n",
" await self.conn.hset(key, mapping=data)\n",
" else:\n",
" # Use HSETNX which will not overwrite existing values\n",
" for field, value in data.items():\n",
" await self.conn.hsetnx(key, field, value)\n",
"\n",
" async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n",
" \"\"\"Get a checkpoint tuple from Redis asynchronously.\n",
@@ -688,21 +717,8 @@
" checkpoint_id\n",
" or _parse_redis_checkpoint_key(checkpoint_key)[\"checkpoint_id\"]\n",
" )\n",
" writes_key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
" )\n",
" matching_keys = await self.conn.keys(pattern=writes_key)\n",
" parsed_keys = [\n",
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
" ]\n",
" pending_writes = _load_writes(\n",
" self.serde,\n",
" {\n",
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): await self.conn.hgetall(key)\n",
" for key, parsed_key in sorted(\n",
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
" )\n",
" },\n",
" pending_writes = await self._aload_pending_writes(\n",
" thread_id, checkpoint_ns, checkpoint_id\n",
" )\n",
" return _parse_redis_checkpoint_data(\n",
" self.serde, checkpoint_key, checkpoint_data, pending_writes=pending_writes\n",
@@ -738,7 +754,36 @@
" for key in keys:\n",
" data = await self.conn.hgetall(key)\n",
" if data and b\"checkpoint\" in data and b\"metadata\" in data:\n",
" yield _parse_redis_checkpoint_data(self.serde, key.decode(), data)\n",
" checkpoint_id = _parse_redis_checkpoint_key(key.decode())[\n",
" \"checkpoint_id\"\n",
" ]\n",
" pending_writes = await self._aload_pending_writes(\n",
" thread_id, checkpoint_ns, checkpoint_id\n",
" )\n",
" yield _parse_redis_checkpoint_data(\n",
" self.serde, key.decode(), data, pending_writes=pending_writes\n",
" )\n",
"\n",
" async def _aload_pending_writes(\n",
" self, thread_id: str, checkpoint_ns: str, checkpoint_id: str\n",
" ) -> List[PendingWrite]:\n",
" writes_key = _make_redis_checkpoint_writes_key(\n",
" thread_id, checkpoint_ns, checkpoint_id, \"*\", None\n",
" )\n",
" matching_keys = await self.conn.keys(pattern=writes_key)\n",
" parsed_keys = [\n",
" _parse_redis_checkpoint_writes_key(key.decode()) for key in matching_keys\n",
" ]\n",
" pending_writes = _load_writes(\n",
" self.serde,\n",
" {\n",
" (parsed_key[\"task_id\"], parsed_key[\"idx\"]): await self.conn.hgetall(key)\n",
" for key, parsed_key in sorted(\n",
" zip(matching_keys, parsed_keys), key=lambda x: x[1][\"idx\"]\n",
" )\n",
" },\n",
" )\n",
" return pending_writes\n",
"\n",
" async def _aget_checkpoint_key(\n",
" self, conn, thread_id: str, checkpoint_ns: str, checkpoint_id: Optional[str]\n",
@@ -1042,7 +1087,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -225,9 +225,19 @@
"# Define the function that responds to the user\n",
"def respond(state: AgentState):\n",
" # Construct the final answer from the arguments of the last tool call\n",
" response = WeatherResponse(**state[\"messages\"][-1].tool_calls[0][\"args\"])\n",
" weather_tool_call = state[\"messages\"][-1].tool_calls[0]\n",
" response = WeatherResponse(**weather_tool_call[\"args\"])\n",
" # Since we're using tool calling to return structured output,\n",
" # we need to add a tool message corresponding to the WeatherResponse tool call,\n",
" # This is due to LLM providers' requirement that AI messages with tool calls\n",
" # need to be followed by a tool message for each tool call\n",
" tool_message = {\n",
" \"type\": \"tool\",\n",
" \"content\": \"Here is your structured response\",\n",
" \"tool_call_id\": weather_tool_call[\"id\"],\n",
" }\n",
" # We return the final answer\n",
" return {\"final_response\": response}\n",
" return {\"final_response\": response, \"messages\": [tool_message]}\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
@@ -466,7 +476,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
+1 -1
View File
@@ -6,7 +6,7 @@
"source": [
"# How to call tools using ToolNode\n",
"\n",
"This guide covers how to use LangGraph's prebuilt [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode) for tool calling.\n",
"This guide covers how to use LangGraph's prebuilt [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode) for tool calling.\n",
"\n",
"`ToolNode` is a LangChain Runnable that takes graph state (with a list of messages) as input and outputs state update with the result of tool calls. It is designed to work well out-of-box with LangGraph's prebuilt [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/), but can also work with any `StateGraph` as long as its state has a `messages` key with an appropriate reducer (see [`MessagesState`](https://github.com/langchain-ai/langgraph/blob/e3ef9adac7395e5c0943c22bbc8a4a856b103aa3/libs/langgraph/langgraph/graph/message.py#L150))."
]
@@ -0,0 +1,383 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7c58c957-83d8-44ff-8580-a9b3dd39a0a9",
"metadata": {},
"source": [
"# How to update graph state from tools"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "95f30587-8dd2-40be-920d-59539089c09f",
"metadata": {},
"source": [
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Command](../../concepts/low_level/#command)\n",
"\n",
"A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={\"my_custom_key\": \"foo\", \"messages\": [...]})` from the tool:\n",
"\n",
"```python\n",
"@tool\n",
"def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):\n",
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
" user_info = get_user_info(config)\n",
" return Command(\n",
" update={\n",
" # update the state keys\n",
" \"user_info\": user_info,\n",
" # update the message history\n",
" \"messages\": [ToolMessage(\"Successfully looked up user information\", tool_call_id=tool_call_id)]\n",
" }\n",
" )\n",
"```\n",
"\n",
"!!! important\n",
"\n",
" If you want to use tools that return `Command` and update graph state, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:\n",
" \n",
" ```python\n",
" def call_tools(state):\n",
" ...\n",
" commands = [tools_by_name[tool_call[\"name\"]].invoke(tool_call) for tool_call in tool_calls]\n",
" return commands\n",
" ```\n",
"\n",
"This guide shows how you can do this using LangGraph's prebuilt components ([`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode]).\n",
"\n",
"!!! note\n",
"\n",
" Support for tools that return [`Command`][langgraph.types.Command] was added in LangGraph `v0.2.59`.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "64500eca-1cdc-43d9-9401-f4cd9999881f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a3f92fb2-9175-47fa-9c7d-ad5f44bfd20e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Please provide your OPENAI_API_KEY ········\n"
]
}
],
"source": [
"import os\n",
"import getpass\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "caf6ff9f-c1e6-499e-a230-9fa231ea7d2f",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "10e9a9c6-fa3f-416c-bac0-3e58d7259908",
"metadata": {},
"source": [
"Let's create a simple ReAct style agent that can look up user information and personalize the response based on the user info."
]
},
{
"cell_type": "markdown",
"id": "4255b9b9-cf67-4cc3-8018-1708f5dfcfd2",
"metadata": {},
"source": [
"## Define tool"
]
},
{
"cell_type": "markdown",
"id": "7de6b010-aab1-4fe8-8251-907fcae78583",
"metadata": {},
"source": [
"First, let's define the tool that we'll be using to look up user information. We'll use a naive implementation that simply looks user information up using a dictionary:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8d070c9f-6e61-4724-85dc-ac4531b9c79a",
"metadata": {},
"outputs": [],
"source": [
"USER_INFO = [\n",
" {\"user_id\": \"1\", \"name\": \"Bob Dylan\", \"location\": \"New York, NY\"},\n",
" {\"user_id\": \"2\", \"name\": \"Taylor Swift\", \"location\": \"Beverly Hills, CA\"},\n",
"]\n",
"\n",
"USER_ID_TO_USER_INFO = {info[\"user_id\"]: info for info in USER_INFO}"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "08d1ecca-ee57-4e97-b8d0-e09de85337d4",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt.chat_agent_executor import AgentState\n",
"from langgraph.types import Command\n",
"from langchain_core.tools import tool\n",
"from langchain_core.tools.base import InjectedToolCallId\n",
"from langchain_core.messages import ToolMessage\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"from typing_extensions import Any, Annotated\n",
"\n",
"\n",
"class State(AgentState):\n",
" # user provided\n",
" last_name: str\n",
" # updated by the tool\n",
" user_info: dict[str, Any]\n",
"\n",
"\n",
"@tool\n",
"def lookup_user_info(\n",
" tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig\n",
"):\n",
" \"\"\"Use this to look up user information to better assist them with their questions.\"\"\"\n",
" user_id = config.get(\"configurable\", {}).get(\"user_id\")\n",
" if user_id is None:\n",
" raise ValueError(\"Please provide user ID\")\n",
"\n",
" if user_id not in USER_ID_TO_USER_INFO:\n",
" raise ValueError(f\"User '{user_id}' not found\")\n",
"\n",
" user_info = USER_ID_TO_USER_INFO[user_id]\n",
" return Command(\n",
" update={\n",
" # update the state keys\n",
" \"user_info\": user_info,\n",
" # update the message history\n",
" \"messages\": [\n",
" ToolMessage(\n",
" \"Successfully looked up user information\", tool_call_id=tool_call_id\n",
" )\n",
" ],\n",
" }\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "b99e5f24-5e5e-4a34-baae-467182675bb5",
"metadata": {},
"source": [
"## Define prompt"
]
},
{
"cell_type": "markdown",
"id": "cbb06aea-6654-4245-91f8-af6e8f2b5377",
"metadata": {},
"source": [
"Let's now add personalization: we'll respond differently to the user based on the state values AFTER the state has been updated from the tool. To achieve this, let's define a function that will dynamically construct the system prompt based on the graph state. It will be called ever time the LLM is called and the function output will be passed to the LLM:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "c553d062-d145-4145-84bd-9b798f7c95c2",
"metadata": {},
"outputs": [],
"source": [
"def state_modifier(state: State):\n",
" user_info = state.get(\"user_info\")\n",
" if user_info is None:\n",
" return state[\"messages\"]\n",
"\n",
" system_msg = (\n",
" f\"User name is {user_info['name']}. User lives in {user_info['location']}\"\n",
" )\n",
" return [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]"
]
},
{
"cell_type": "markdown",
"id": "c5acdd5d-68be-466b-9c21-46cbed91d2bc",
"metadata": {},
"source": [
"## Define graph"
]
},
{
"cell_type": "markdown",
"id": "afb65028-0359-46c8-b09c-ffc90180f759",
"metadata": {},
"source": [
"Finally, let's combine this into a single graph using the prebuilt `create_react_agent`:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "2d59db29-fd51-4d29-9854-21763a4855e3",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\")\n",
"\n",
"agent = create_react_agent(\n",
" model,\n",
" # pass the tool that can update state\n",
" [lookup_user_info],\n",
" state_schema=State,\n",
" # pass dynamic prompt function\n",
" state_modifier=state_modifier,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "0782b8ab-a603-47b8-9a76-77f593402678",
"metadata": {},
"source": [
"## Use it!"
]
},
{
"cell_type": "markdown",
"id": "6165e153-ab28-4404-adea-796c7bd0701b",
"metadata": {},
"source": [
"Let's now try running our agent. We'll need to provide user ID in the config so that our tool knows what information to look up:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "de34a58b-1765-4b63-a232-d46790aff884",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-57eeb216-e35d-4501-aaac-b5c6b26fb17c-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_7LSUh6ZDvGJAUvlWvXiCK4Gf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n",
"{'tools': {'user_info': {'user_id': '1', 'name': 'Bob Dylan', 'location': 'New York, NY'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='168d8ff8-b021-4c8b-a11a-3b50c30a072c', tool_call_id='call_7LSUh6ZDvGJAUvlWvXiCK4Gf')]}}\n",
"\n",
"\n",
"{'agent': {'messages': [AIMessage(content=\"Hi Bob! Since you're in New York, NY, there are plenty of exciting things to do over the weekend. Here are some suggestions:\\n\\n1. **Explore Central Park**: Take a leisurely walk, rent a bike, or have a picnic in this iconic park.\\n\\n2. **Visit a Museum**: Check out The Metropolitan Museum of Art or the Museum of Modern Art (MoMA) for an enriching cultural experience.\\n\\n3. **Broadway Show**: Catch a Broadway show or an off-Broadway performance for some world-class entertainment.\\n\\n4. **Food Tour**: Explore different neighborhoods like Greenwich Village or Williamsburg for diverse culinary experiences.\\n\\n5. **Brooklyn Bridge Walk**: Take a walk across the Brooklyn Bridge for stunning views of the city skyline.\\n\\n6. **Visit a Rooftop Bar**: Enjoy a drink with a view at one of New Yorks many rooftop bars.\\n\\n7. **Explore a New Neighborhood**: Discover the unique charm of areas like SoHo, Chelsea, or Astoria.\\n\\n8. **Live Music**: Check out live music venues for a night of great performances.\\n\\n9. **Art Galleries**: Visit some of the smaller art galleries around Chelsea or the Lower East Side.\\n\\n10. **Attend a Local Event**: Look up any local events or festivals happening this weekend.\\n\\nFeel free to let me know if you want more details on any of these activities!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 285, 'prompt_tokens': 95, 'total_tokens': 380, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_9d50cd990b', 'finish_reason': 'stop', 'logprobs': None}, id='run-f13ce15b-02b6-40e6-8264-c4d9edd0d03a-0', usage_metadata={'input_tokens': 95, 'output_tokens': 285, 'total_tokens': 380, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in agent.stream(\n",
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
" # provide user ID in the config\n",
" {\"configurable\": {\"user_id\": \"1\"}},\n",
"):\n",
" print(chunk)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "d9b2281f-269c-41dd-b6b2-4c743f11ffc9",
"metadata": {},
"source": [
"We can see that the model correctly recommended some New York activities for Bob Dylan! Let's try getting recommendations for Taylor Swift:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "9d71af94-572a-4961-88a7-665e792cf96a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'function': {'arguments': '{}', 'name': 'lookup_user_info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 56, 'total_tokens': 67, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-bacacd7d-76cc-4f6b-9e9b-d9e6f00b9391-0', tool_calls=[{'name': 'lookup_user_info', 'args': {}, 'id': 'call_5HLtJtzcgmKbtmK6By21wW5Y', 'type': 'tool_call'}], usage_metadata={'input_tokens': 56, 'output_tokens': 11, 'total_tokens': 67, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n",
"{'tools': {'user_info': {'user_id': '2', 'name': 'Taylor Swift', 'location': 'Beverly Hills, CA'}, 'messages': [ToolMessage(content='Successfully looked up user information', name='lookup_user_info', id='d81ef31e-6d77-4f13-ae86-e2e6ba567e3d', tool_call_id='call_5HLtJtzcgmKbtmK6By21wW5Y')]}}\n",
"\n",
"\n",
"{'agent': {'messages': [AIMessage(content=\"Hi Taylor! Since you're in Beverly Hills, here are a few suggestions for a fun weekend:\\n\\n1. **Hiking at Runyon Canyon**: Enjoy a scenic hike with beautiful views of Los Angeles. It's a great way to get some exercise and enjoy the outdoors.\\n\\n2. **Visit Rodeo Drive**: Spend some time shopping or window shopping at the famous Rodeo Drive. You might even spot some celebrities!\\n\\n3. **Explore the Getty Center**: Check out the art collections and beautiful gardens at the Getty Center. The architecture and views are stunning.\\n\\n4. **Relax at a Spa**: Treat yourself to a relaxing day at one of Beverly Hills' luxurious spas.\\n\\n5. **Dining Out**: Try a new restaurant or visit your favorite spot for a delicious meal. Beverly Hills has a fantastic dining scene.\\n\\n6. **Attend a Local Event**: Check out any local events or concerts happening this weekend. Beverly Hills often hosts exciting events.\\n\\nEnjoy your weekend!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 198, 'prompt_tokens': 95, 'total_tokens': 293, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_c7ca0ebaca', 'finish_reason': 'stop', 'logprobs': None}, id='run-2057df76-f192-4c69-a66a-1f0a86bf5d66-0', usage_metadata={'input_tokens': 95, 'output_tokens': 198, 'total_tokens': 293, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in agent.stream(\n",
" {\"messages\": [(\"user\", \"hi, what should i do this weekend?\")]},\n",
" {\"configurable\": {\"user_id\": \"2\"}},\n",
"):\n",
" print(chunk)\n",
" print(\"\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
@@ -289,15 +289,10 @@
"from langchain_core.language_models.chat_models import BaseChatModel\n",
"\n",
"from langgraph.graph import StateGraph, MessagesState, START, END\n",
"from langgraph.types import Command\n",
"from langchain_core.messages import HumanMessage, trim_messages\n",
"\n",
"\n",
"# The agent state is the input to each node in the graph\n",
"class AgentState(MessagesState):\n",
" # The 'next' field indicates where to route to next\n",
" next: str\n",
"\n",
"\n",
"def make_supervisor_node(llm: BaseChatModel, members: list[str]) -> str:\n",
" options = [\"FINISH\"] + members\n",
" system_prompt = (\n",
@@ -313,17 +308,17 @@
"\n",
" next: Literal[*options]\n",
"\n",
" def supervisor_node(state: MessagesState) -> MessagesState:\n",
" def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
" \"\"\"An LLM-based router.\"\"\"\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": system_prompt},\n",
" ] + state[\"messages\"]\n",
" response = llm.with_structured_output(Router).invoke(messages)\n",
" next_ = response[\"next\"]\n",
" if next_ == \"FINISH\":\n",
" next_ = END\n",
" goto = response[\"next\"]\n",
" if goto == \"FINISH\":\n",
" goto = END\n",
"\n",
" return {\"next\": next_}\n",
" return Command(goto=goto)\n",
"\n",
" return supervisor_node"
]
@@ -363,25 +358,33 @@
"search_agent = create_react_agent(llm, tools=[tavily_tool])\n",
"\n",
"\n",
"def search_node(state: AgentState) -> AgentState:\n",
"def search_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" result = search_agent.invoke(state)\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"search\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"search\")\n",
" ]\n",
" },\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"web_scraper_agent = create_react_agent(llm, tools=[scrape_webpages])\n",
"\n",
"\n",
"def web_scraper_node(state: AgentState) -> AgentState:\n",
"def web_scraper_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" result = web_scraper_agent.invoke(state)\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"web_scraper\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"web_scraper\")\n",
" ]\n",
" },\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"research_supervisor_node = make_supervisor_node(llm, [\"search\", \"web_scraper\"])"
@@ -412,14 +415,7 @@
"research_builder.add_node(\"search\", search_node)\n",
"research_builder.add_node(\"web_scraper\", web_scraper_node)\n",
"\n",
"# Define the control flow\n",
"research_builder.add_edge(START, \"supervisor\")\n",
"# We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
"research_builder.add_edge(\"search\", \"supervisor\")\n",
"research_builder.add_edge(\"web_scraper\", \"supervisor\")\n",
"# Add the edges where routing applies\n",
"research_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
"\n",
"research_graph = research_builder.compile()"
]
},
@@ -532,13 +528,17 @@
")\n",
"\n",
"\n",
"def doc_writing_node(state: AgentState) -> AgentState:\n",
"def doc_writing_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" result = doc_writer_agent.invoke(state)\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"doc_writer\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"doc_writer\")\n",
" ]\n",
" },\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"note_taking_agent = create_react_agent(\n",
@@ -551,13 +551,17 @@
")\n",
"\n",
"\n",
"def note_taking_node(state: AgentState) -> AgentState:\n",
"def note_taking_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" result = note_taking_agent.invoke(state)\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"note_taker\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"note_taker\")\n",
" ]\n",
" },\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"chart_generating_agent = create_react_agent(\n",
@@ -565,13 +569,19 @@
")\n",
"\n",
"\n",
"def chart_generating_node(state: AgentState) -> AgentState:\n",
"def chart_generating_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" result = chart_generating_agent.invoke(state)\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=result[\"messages\"][-1].content, name=\"chart_generator\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=result[\"messages\"][-1].content, name=\"chart_generator\"\n",
" )\n",
" ]\n",
" },\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"doc_writing_supervisor_node = make_supervisor_node(\n",
@@ -600,21 +610,13 @@
"outputs": [],
"source": [
"# Create the graph here\n",
"paper_writing_builder = StateGraph(AgentState)\n",
"paper_writing_builder = StateGraph(MessagesState)\n",
"paper_writing_builder.add_node(\"supervisor\", doc_writing_supervisor_node)\n",
"paper_writing_builder.add_node(\"doc_writer\", doc_writing_node)\n",
"paper_writing_builder.add_node(\"note_taker\", note_taking_node)\n",
"paper_writing_builder.add_node(\"chart_generator\", chart_generating_node)\n",
"\n",
"# Define the control flow\n",
"paper_writing_builder.add_edge(START, \"supervisor\")\n",
"# We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
"paper_writing_builder.add_edge(\"doc_writer\", \"supervisor\")\n",
"paper_writing_builder.add_edge(\"note_taker\", \"supervisor\")\n",
"paper_writing_builder.add_edge(\"chart_generator\", \"supervisor\")\n",
"# Add the edges where routing applies\n",
"paper_writing_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
"\n",
"paper_writing_graph = paper_writing_builder.compile()"
]
},
@@ -728,37 +730,41 @@
},
"outputs": [],
"source": [
"def call_research_team(state: AgentState) -> AgentState:\n",
"def call_research_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" response = research_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=response[\"messages\"][-1].content, name=\"research_team\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=response[\"messages\"][-1].content, name=\"research_team\"\n",
" )\n",
" ]\n",
" },\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"def call_paper_writing_team(state: AgentState) -> AgentState:\n",
"def call_paper_writing_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
" response = paper_writing_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
" return {\n",
" \"messages\": [\n",
" HumanMessage(content=response[\"messages\"][-1].content, name=\"writing_team\")\n",
" ]\n",
" }\n",
" return Command(\n",
" update={\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=response[\"messages\"][-1].content, name=\"writing_team\"\n",
" )\n",
" ]\n",
" },\n",
" goto=\"supervisor\",\n",
" )\n",
"\n",
"\n",
"# Define the graph.\n",
"super_builder = StateGraph(AgentState)\n",
"super_builder = StateGraph(MessagesState)\n",
"super_builder.add_node(\"supervisor\", teams_supervisor_node)\n",
"super_builder.add_node(\"research_team\", call_research_team)\n",
"super_builder.add_node(\"writing_team\", call_paper_writing_team)\n",
"\n",
"# Define the control flow\n",
"super_builder.add_edge(START, \"supervisor\")\n",
"# We want our teams to ALWAYS \"report back\" to the top-level supervisor when done\n",
"super_builder.add_edge(\"research_team\", \"supervisor\")\n",
"super_builder.add_edge(\"writing_team\", \"supervisor\")\n",
"# Add the edges where routing applies\n",
"super_builder.add_conditional_edges(\"supervisor\", lambda state: state[\"next\"])\n",
"super_graph = super_builder.compile()"
]
},
File diff suppressed because one or more lines are too long
+4
View File
@@ -192,6 +192,7 @@ nav:
- how-tos/tool-calling.ipynb
- how-tos/tool-calling-errors.ipynb
- how-tos/pass-run-time-values-to-tools.ipynb
- how-tos/update-state-from-tools.ipynb
- how-tos/pass-config-to-tools.ipynb
- how-tos/many-tools.ipynb
- Subgraphs:
@@ -199,6 +200,8 @@ nav:
- how-tos/subgraph.ipynb
- how-tos/subgraphs-manage-state.ipynb
- how-tos/subgraph-transform-state.ipynb
- Multi-agent:
- how-tos/multi-agent-network.ipynb
- State Management:
- State Management: how-tos#state-management
- how-tos/state-model.ipynb
@@ -281,6 +284,7 @@ nav:
- cloud/how-tos/test_local_deployment.md
- cloud/how-tos/invoke_studio.md
- cloud/how-tos/threads_studio.md
- cloud/how-tos/datasets_studio.md
- Troubleshooting:
- Troubleshooting: how-tos#troubleshooting
- troubleshooting/errors/index.md
@@ -155,9 +155,6 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
return results
def batch(self, ops: Iterable[Op]) -> list[Result]:
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
@classmethod
@asynccontextmanager
async def from_conn_string(
@@ -1,8 +1,10 @@
# type: ignore
import asyncio
import itertools
import sys
import uuid
from collections.abc import AsyncIterator
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
from typing import Any, Optional
@@ -10,7 +12,13 @@ import pytest
from langchain_core.embeddings import Embeddings
from psycopg import AsyncConnection
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.base import (
GetOp,
Item,
ListNamespacesOp,
PutOp,
SearchOp,
)
from langgraph.store.postgres import AsyncPostgresStore
from tests.conftest import (
DEFAULT_URI,
@@ -63,6 +71,128 @@ async def store(request) -> AsyncIterator[AsyncPostgresStore]:
await conn.execute(f"DROP DATABASE {database}")
async def test_no_running_loop(store: AsyncPostgresStore) -> None:
with pytest.raises(asyncio.InvalidStateError):
store.put(("foo", "bar"), "baz", {"val": "baz"})
with pytest.raises(asyncio.InvalidStateError):
store.get(("foo", "bar"), "baz")
with pytest.raises(asyncio.InvalidStateError):
store.delete(("foo", "bar"), "baz")
with pytest.raises(asyncio.InvalidStateError):
store.search(("foo", "bar"))
with pytest.raises(asyncio.InvalidStateError):
store.list_namespaces(prefix=("foo",))
with pytest.raises(asyncio.InvalidStateError):
store.batch([PutOp(namespace=("foo", "bar"), key="baz", value={"val": "baz"})])
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(store.put, ("foo", "bar"), "baz", {"val": "baz"})
result = await asyncio.wrap_future(future)
assert result is None
future = executor.submit(store.get, ("foo", "bar"), "baz")
result = await asyncio.wrap_future(future)
assert result.value == {"val": "baz"}
result = await asyncio.wrap_future(
executor.submit(store.list_namespaces, prefix=("foo",))
)
async def test_large_batches(request: Any, store: AsyncPostgresStore) -> None:
N = 100 # less important that we are performant here
M = 10
with ThreadPoolExecutor(max_workers=10) as executor:
futures = []
for m in range(M):
for i in range(N):
futures += [
executor.submit(
store.put,
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
value={"foo": "bar" + str(i)},
),
executor.submit(
store.get,
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
),
executor.submit(
store.list_namespaces,
prefix=None,
max_depth=m + 1,
),
executor.submit(
store.search,
("test",),
),
executor.submit(
store.put,
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
value={"foo": "bar" + str(i)},
),
executor.submit(
store.put,
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
None,
),
]
results = await asyncio.gather(
*(asyncio.wrap_future(future) for future in futures)
)
assert len(results) == M * N * 6
async def test_large_batches_async(store: AsyncPostgresStore) -> None:
N = 1000
M = 10
coros = []
for m in range(M):
for i in range(N):
coros.append(
store.aput(
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
value={"foo": "bar" + str(i)},
)
)
coros.append(
store.aget(
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
)
)
coros.append(
store.alist_namespaces(
prefix=None,
max_depth=m + 1,
)
)
coros.append(
store.asearch(
("test",),
)
)
coros.append(
store.aput(
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
value={"foo": "bar" + str(i)},
)
)
coros.append(
store.adelete(
("test", "foo", "bar", "baz", str(m % 2)),
f"key{i}",
)
)
results = await asyncio.gather(*coros)
assert len(results) == M * N * 6
async def test_abatch_order(store: AsyncPostgresStore) -> None:
# Setup test data
await store.aput(("test", "foo"), "key1", {"data": "value1"})
@@ -1,4 +1,3 @@
import asyncio
import logging
import os
import pickle
@@ -6,7 +5,6 @@ import random
import shutil
from collections import defaultdict
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
from functools import partial
from types import TracebackType
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
@@ -395,9 +393,7 @@ class MemorySaver(
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
return await asyncio.get_running_loop().run_in_executor(
None, self.get_tuple, config
)
return self.get_tuple(config)
async def alist(
self,
@@ -418,24 +414,8 @@ class MemorySaver(
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
"""
loop = asyncio.get_running_loop()
iter = await loop.run_in_executor(
None,
partial(
self.list,
before=before,
limit=limit,
filter=filter,
),
config,
)
while True:
# handling StopIteration exception inside coroutine won't work
# as expected, so using next() with default value to break the loop
if item := await loop.run_in_executor(None, next, iter, None):
yield item
else:
break
for item in self.list(config, filter=filter, before=before, limit=limit):
yield item
async def aput(
self,
@@ -455,9 +435,7 @@ class MemorySaver(
Returns:
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
"""
return await asyncio.get_running_loop().run_in_executor(
None, self.put, config, checkpoint, metadata, new_versions
)
return self.put(config, checkpoint, metadata, new_versions)
async def aput_writes(
self,
@@ -474,10 +452,9 @@ class MemorySaver(
config (RunnableConfig): The config to associate with the writes.
writes (List[Tuple[str, Any]]): The writes to save, each as a (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
return self.put_writes(config, writes, task_id)
"""
return await asyncio.get_running_loop().run_in_executor(
None, self.put_writes, config, writes, task_id
)
return self.put_writes(config, writes, task_id)
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
if current is None:
@@ -438,28 +438,36 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
def _msgpack_ext_hook(code: int, data: bytes) -> Any:
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, arg
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
except Exception:
return
elif code == EXT_CONSTRUCTOR_POS_ARGS:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, args
return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
except Exception:
return
elif code == EXT_CONSTRUCTOR_KW_ARGS:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, args
return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
except Exception:
return
elif code == EXT_METHOD_SINGLE_ARG:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, arg, method
return getattr(getattr(importlib.import_module(tup[0]), tup[1]), tup[3])(
tup[2]
@@ -468,7 +476,9 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
return
elif code == EXT_PYDANTIC_V1:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, kwargs
cls = getattr(importlib.import_module(tup[0]), tup[1])
try:
@@ -479,7 +489,9 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
return
elif code == EXT_PYDANTIC_V2:
try:
tup = msgpack.unpackb(data, ext_hook=_msgpack_ext_hook)
tup = msgpack.unpackb(
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
)
# module, name, kwargs, method
cls = getattr(importlib.import_module(tup[0]), tup[1])
try:
+109 -2
View File
@@ -1,6 +1,7 @@
import asyncio
import functools
import weakref
from typing import Any, Literal, Optional, Union
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
from langgraph.store.base import (
BaseStore,
@@ -11,11 +12,39 @@ from langgraph.store.base import (
NamespacePath,
Op,
PutOp,
Result,
SearchItem,
SearchOp,
_validate_namespace,
)
F = TypeVar("F", bound=Callable)
def _check_loop(func: F) -> F:
@functools.wraps(func)
def wrapper(store: "AsyncBatchedBaseStore", *args: Any, **kwargs: Any) -> Any:
method_name: str = func.__name__
try:
current_loop = asyncio.get_running_loop()
if current_loop is store._loop:
replacement_str = (
f"Specifically, replace `store.{method_name}(...)` with `await store.a{method_name}(...)"
if method_name
else "For example, replace `store.get(...)` with `await store.aget(...)`"
)
raise asyncio.InvalidStateError(
f"Synchronous calls to {store.__class__.__name__} detected in the main event loop. "
"This can lead to deadlocks or performance issues. "
"Please use the asynchronous interface for main thread operations. "
f"{replacement_str} "
)
except RuntimeError:
pass
return func(store, *args, **kwargs)
return wrapper
class AsyncBatchedBaseStore(BaseStore):
"""Efficiently batch operations in a background task."""
@@ -23,6 +52,7 @@ class AsyncBatchedBaseStore(BaseStore):
__slots__ = ("_loop", "_aqueue", "_task")
def __init__(self) -> None:
super().__init__()
self._loop = asyncio.get_running_loop()
self._aqueue: dict[asyncio.Future, Op] = {}
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
@@ -99,6 +129,82 @@ class AsyncBatchedBaseStore(BaseStore):
self._aqueue[fut] = op
return await fut
@_check_loop
def batch(self, ops: Iterable[Op]) -> list[Result]:
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self._loop).result()
@_check_loop
def get(
self,
namespace: tuple[str, ...],
key: str,
) -> Optional[Item]:
return asyncio.run_coroutine_threadsafe(
self.aget(namespace, key=key), self._loop
).result()
@_check_loop
def search(
self,
namespace_prefix: tuple[str, ...],
/,
*,
query: Optional[str] = None,
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
) -> list[SearchItem]:
return asyncio.run_coroutine_threadsafe(
self.asearch(
namespace_prefix, query=query, filter=filter, limit=limit, offset=offset
),
self._loop,
).result()
@_check_loop
def put(
self,
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
_validate_namespace(namespace)
asyncio.run_coroutine_threadsafe(
self.aput(namespace, key=key, value=value, index=index), self._loop
).result()
@_check_loop
def delete(
self,
namespace: tuple[str, ...],
key: str,
) -> None:
asyncio.run_coroutine_threadsafe(
self.adelete(namespace, key=key), self._loop
).result()
@_check_loop
def list_namespaces(
self,
*,
prefix: Optional[NamespacePath] = None,
suffix: Optional[NamespacePath] = None,
max_depth: Optional[int] = None,
limit: int = 100,
offset: int = 0,
) -> list[tuple[str, ...]]:
return asyncio.run_coroutine_threadsafe(
self.alist_namespaces(
prefix=prefix,
suffix=suffix,
max_depth=max_depth,
limit=limit,
offset=offset,
),
self._loop,
).result()
def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
"""Dedupe operations while preserving order for results.
@@ -144,7 +250,8 @@ def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
async def _run(
aqueue: dict[asyncio.Future, Op], store: weakref.ReferenceType[BaseStore]
aqueue: dict[asyncio.Future, Op],
store: weakref.ReferenceType[BaseStore],
) -> None:
while True:
await asyncio.sleep(0)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.8"
version = "2.0.9"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+11 -8
View File
@@ -576,16 +576,18 @@ def dev(
from langgraph_api.cli import run_server
except ImportError:
try:
import pkg_resources
from importlib import util
pkg_resources.require("langgraph-api-inmem")
except (ImportError, pkg_resources.DistributionNotFound):
if not util.find_spec("langgraph_api"):
raise click.UsageError(
"Required package 'langgraph-api' is not installed.\n"
"Please install it with:\n\n"
' pip install -U "langgraph-cli[inmem]"\n\n'
) from None
except ImportError:
raise click.UsageError(
"Required package 'langgraph-api-inmem' is not installed.\n"
"Please install it with:\n\n"
' pip install -U "langgraph-cli[inmem]"\n\n'
"If you're developing the langgraph-cli package locally, you can install in development mode:\n"
" pip install -e ."
"Could not verify package installation. Please ensure Python is up to date and\n"
"langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
) from None
raise click.UsageError(
"Could not import run_server. This likely means your installation is incomplete.\n"
@@ -614,6 +616,7 @@ def dev(
env=config_json.get("env"),
store=config_json.get("store"),
wait_for_client=wait_for_client,
auth=config_json.get("auth"),
)
+52 -1
View File
@@ -47,6 +47,44 @@ class StoreConfig(TypedDict, total=False):
"""Configuration for vector embeddings in store."""
class SecurityConfig(TypedDict, total=False):
securitySchemes: dict
security: list
# path => {method => security}
paths: dict[str, dict[str, list]]
class AuthConfig(TypedDict, total=False):
path: str
"""Path to the authentication function in a Python file."""
disable_studio_auth: bool
"""Whether to disable auth when connecting from the LangSmith Studio."""
openapi: SecurityConfig
"""The schema to use for updating the openapi spec.
Example:
{
"securitySchemes": {
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {
"me": "Read information about the current user",
"items": "Access to create and manage items"
}
}
}
}
},
"security": [
{"OAuth2": ["me"]} # Default security requirement for all endpoints
]
}
"""
class Config(TypedDict, total=False):
python_version: str
node_version: Optional[str]
@@ -56,6 +94,7 @@ class Config(TypedDict, total=False):
graphs: dict[str, str]
env: Union[dict[str, str], str]
store: Optional[StoreConfig]
auth: Optional[AuthConfig]
def _parse_version(version_str: str) -> tuple[int, int]:
@@ -85,9 +124,11 @@ def validate_config(config: Config) -> Config:
{
"node_version": config.get("node_version"),
"dockerfile_lines": config.get("dockerfile_lines", []),
"dependencies": config.get("dependencies", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
}
if config.get("node_version")
else {
@@ -98,6 +139,7 @@ def validate_config(config: Config) -> Config:
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
}
)
@@ -400,6 +442,10 @@ RUN set -ex && \\
ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
"""
)
if (auth_config := config.get("auth")) is not None:
env_additional_config += f"""
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
"""
return f"""FROM {base_image}:{config['python_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -445,6 +491,10 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
"""
)
if (auth_config := config.get("auth")) is not None:
env_additional_config += f"""
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
"""
return f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -479,8 +529,9 @@ def config_to_compose(
f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
)
if watch:
dependencies = config.get("dependencies") or ["."]
watch_paths = [config_path.name] + [
dep for dep in config["dependencies"] if dep.startswith(".")
dep for dep in dependencies if dep.startswith(".")
]
watch_actions = "\n".join(
f"""- path: {path}
+104 -116
View File
@@ -13,22 +13,23 @@ files = [
[[package]]
name = "anyio"
version = "4.6.2.post1"
version = "4.7.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = true
python-versions = ">=3.9"
files = [
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
trio = ["trio (>=0.26.1)"]
[[package]]
@@ -384,13 +385,13 @@ trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.27.2"
version = "0.28.1"
description = "The next generation HTTP client."
optional = true
python-versions = ">=3.8"
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
@@ -398,7 +399,6 @@ anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
@@ -407,17 +407,6 @@ http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "httpx-sse"
version = "0.4.0"
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
optional = true
python-versions = ">=3.8"
files = [
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
]
[[package]]
name = "idna"
version = "3.10"
@@ -526,18 +515,18 @@ tests = ["flask (>=2.2.5)", "hypothesis (>=6.79.4)", "pytest (>=7.4.4)"]
[[package]]
name = "langchain-core"
version = "0.3.21"
version = "0.3.24"
description = "Building applications with LLMs through composability"
optional = true
python-versions = "<4.0,>=3.9"
files = [
{file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
{file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
{file = "langchain_core-0.3.24-py3-none-any.whl", hash = "sha256:97192552ef882a3dd6ae3b870a180a743801d0137a1159173f51ac555eeb7eec"},
{file = "langchain_core-0.3.24.tar.gz", hash = "sha256:460851e8145327f70b70aad7dce2cdbd285e144d14af82b677256b941fc99656"},
]
[package.dependencies]
jsonpatch = ">=1.33,<2.0"
langsmith = ">=0.1.125,<0.2.0"
langsmith = ">=0.1.125,<0.3"
packaging = ">=23.2,<25"
pydantic = [
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
@@ -549,29 +538,29 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph"
version = "0.2.53"
version = "0.2.59"
description = "Building stateful, multi-actor applications with LLMs"
optional = true
python-versions = "<4.0,>=3.9.0"
files = [
{file = "langgraph-0.2.53-py3-none-any.whl", hash = "sha256:b34b67d0a12ae0ba6f03af97ad0f744bc609bd0328e8b734618cc039985cfdea"},
{file = "langgraph-0.2.53.tar.gz", hash = "sha256:b83232a04f2b536cbeac542f9ad7e0265f41ac6b7c6706ba8e031e0e80cb13a6"},
{file = "langgraph-0.2.59-py3-none-any.whl", hash = "sha256:9b2d1331bbdcea96cfffde8e88700776118f4583d15f6a3423fc4482fe84ae56"},
{file = "langgraph-0.2.59.tar.gz", hash = "sha256:61cb5dd409878641fe8a12e9d7928a7524caa3e54ab12bc883feeaba4a5ba618"},
]
[package.dependencies]
langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.4.0"
langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.3.15 || >0.3.15,<0.3.16 || >0.3.16,<0.3.17 || >0.3.17,<0.3.18 || >0.3.18,<0.3.19 || >0.3.19,<0.3.20 || >0.3.20,<0.3.21 || >0.3.21,<0.3.22 || >0.3.22,<0.4.0"
langgraph-checkpoint = ">=2.0.4,<3.0.0"
langgraph-sdk = ">=0.1.32,<0.2.0"
langgraph-sdk = ">=0.1.42,<0.2.0"
[[package]]
name = "langgraph-api"
version = "0.0.6"
version = "0.0.8"
description = ""
optional = true
python-versions = "<4.0,>=3.11.0"
files = [
{file = "langgraph_api-0.0.6-py3-none-any.whl", hash = "sha256:f64b13959d721143f6a023af5b9ffc9aa054064af98d21d5d8090cda7e7bffd2"},
{file = "langgraph_api-0.0.6.tar.gz", hash = "sha256:badac44fa1ec979509e56fc0da57eeb5f278ee5871f27803f73ea6d8822c21b9"},
{file = "langgraph_api-0.0.8-py3-none-any.whl", hash = "sha256:ab9b4a5ec8393d17ef921b94f3f019f49f3f17910ba9351beb1e1f962b02a33e"},
{file = "langgraph_api-0.0.8.tar.gz", hash = "sha256:b1304beb9d72392ce5d5602cd524a4f57839f1dcd1da6f4e1391832fb4a6edc5"},
]
[package.dependencies]
@@ -579,7 +568,7 @@ cryptography = ">=43.0.3,<44.0.0"
httpx = ">=0.27.0"
jsonschema-rs = ">=0.25.0,<0.26.0"
langchain-core = ">=0.2.38,<0.4.0"
langgraph = ">=0.2.52,<0.3.0"
langgraph = ">=0.2.56,<0.3.0"
langgraph-checkpoint = ">=2.0.7,<3.0"
langsmith = ">=0.1.63,<0.2.0"
orjson = ">=3.10.1"
@@ -593,13 +582,13 @@ watchfiles = ">=0.13"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.7"
version = "2.0.9"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = true
python-versions = "<4.0.0,>=3.9.0"
files = [
{file = "langgraph_checkpoint-2.0.7-py3-none-any.whl", hash = "sha256:9709f672e1c5a47e13352067c2ffa114dd91d443967b7ce8a1d36d6fc170370e"},
{file = "langgraph_checkpoint-2.0.7.tar.gz", hash = "sha256:88d648a331d20aa8ce65280de34a34a9190380b004f6afcc5f9894fe3abeed08"},
{file = "langgraph_checkpoint-2.0.9-py3-none-any.whl", hash = "sha256:b546ed6129929b8941ac08af6ce5cd26c8ebe1d25883d3c48638d34ade91ce42"},
{file = "langgraph_checkpoint-2.0.9.tar.gz", hash = "sha256:43847d7e385a2d9d2b684155920998e44ed42d2d1780719e4f6111fe3d6db84c"},
]
[package.dependencies]
@@ -608,18 +597,17 @@ msgpack = ">=1.1.0,<2.0.0"
[[package]]
name = "langgraph-sdk"
version = "0.1.40"
version = "0.1.44"
description = "SDK for interacting with LangGraph API"
optional = true
python-versions = "<4.0.0,>=3.9.0"
files = [
{file = "langgraph_sdk-0.1.40-py3-none-any.whl", hash = "sha256:8810cca5e4144cf3a5441fc76b4ee6e658ec95f932d3a0bf9ad63de117e925b9"},
{file = "langgraph_sdk-0.1.40.tar.gz", hash = "sha256:ab2719ac7274612a791a7a0ad9395d250357106cba8ba81bca9968fc91009af2"},
{file = "langgraph_sdk-0.1.44-py3-none-any.whl", hash = "sha256:a5a623429b44616c4369c0caf2ea30ce8d3a103123f7bb3ca50dc726eaa313a4"},
{file = "langgraph_sdk-0.1.44.tar.gz", hash = "sha256:5b17fe7c0a0fe4b83170c9b8987e0f98dc0636fcc0166dbdba725cc8ffd9b1cf"},
]
[package.dependencies]
httpx = ">=0.25.2"
httpx-sse = ">=0.4.0"
orjson = ">=3.10.1"
[[package]]
@@ -906,13 +894,13 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.2"
version = "2.10.3"
description = "Data validation using Python type hints"
optional = true
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
{file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
]
[package.dependencies]
@@ -1474,82 +1462,82 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "watchfiles"
version = "1.0.0"
version = "1.0.3"
description = "Simple, modern and high performance file watching and code reload in python."
optional = true
python-versions = ">=3.9"
files = [
{file = "watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f"},
{file = "watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105"},
{file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236"},
{file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2"},
{file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935"},
{file = "watchfiles-1.0.0-cp310-none-win32.whl", hash = "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755"},
{file = "watchfiles-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e"},
{file = "watchfiles-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269"},
{file = "watchfiles-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4"},
{file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"},
{file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a"},
{file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d"},
{file = "watchfiles-1.0.0-cp311-none-win32.whl", hash = "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62"},
{file = "watchfiles-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84"},
{file = "watchfiles-1.0.0-cp311-none-win_arm64.whl", hash = "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817"},
{file = "watchfiles-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107"},
{file = "watchfiles-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f"},
{file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514"},
{file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81"},
{file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00"},
{file = "watchfiles-1.0.0-cp312-none-win32.whl", hash = "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c"},
{file = "watchfiles-1.0.0-cp312-none-win_amd64.whl", hash = "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545"},
{file = "watchfiles-1.0.0-cp312-none-win_arm64.whl", hash = "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82"},
{file = "watchfiles-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152"},
{file = "watchfiles-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525"},
{file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285"},
{file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb"},
{file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6"},
{file = "watchfiles-1.0.0-cp313-none-win32.whl", hash = "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98"},
{file = "watchfiles-1.0.0-cp313-none-win_amd64.whl", hash = "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941"},
{file = "watchfiles-1.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0"},
{file = "watchfiles-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399"},
{file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f"},
{file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919"},
{file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b"},
{file = "watchfiles-1.0.0-cp39-none-win32.whl", hash = "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3"},
{file = "watchfiles-1.0.0-cp39-none-win_amd64.whl", hash = "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855"},
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b"},
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d"},
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7"},
{file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0"},
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e"},
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c"},
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17"},
{file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc"},
{file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"},
{file = "watchfiles-1.0.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da46bb1eefb5a37a8fb6fd52ad5d14822d67c498d99bda8754222396164ae42"},
{file = "watchfiles-1.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b961b86cd3973f5822826017cad7f5a75795168cb645c3a6b30c349094e02e3"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34e87c7b3464d02af87f1059fedda5484e43b153ef519e4085fe1a03dd94801e"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9dd2b89a16cf7ab9c1170b5863e68de6bf83db51544875b25a5f05a7269e678"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b4691234d31686dca133c920f94e478b548a8e7c750f28dbbc2e4333e0d3da9"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90b0fe1fcea9bd6e3084b44875e179b4adcc4057a3b81402658d0eb58c98edf8"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b90651b4cf9e158d01faa0833b073e2e37719264bcee3eac49fc3c74e7d304b"},
{file = "watchfiles-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2e9fe695ff151b42ab06501820f40d01310fbd58ba24da8923ace79cf6d702d"},
{file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62691f1c0894b001c7cde1195c03b7801aaa794a837bd6eef24da87d1542838d"},
{file = "watchfiles-1.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:275c1b0e942d335fccb6014d79267d1b9fa45b5ac0639c297f1e856f2f532552"},
{file = "watchfiles-1.0.3-cp310-cp310-win32.whl", hash = "sha256:06ce08549e49ba69ccc36fc5659a3d0ff4e3a07d542b895b8a9013fcab46c2dc"},
{file = "watchfiles-1.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f280b02827adc9d87f764972fbeb701cf5611f80b619c20568e1982a277d6146"},
{file = "watchfiles-1.0.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ffe709b1d0bc2e9921257569675674cafb3a5f8af689ab9f3f2b3f88775b960f"},
{file = "watchfiles-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:418c5ce332f74939ff60691e5293e27c206c8164ce2b8ce0d9abf013003fb7fe"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f492d2907263d6d0d52f897a68647195bc093dafed14508a8d6817973586b6b"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c9f3bc90c556a854f4cab6a79c16974099ccfa3e3e150673d82d47a4bc92c9"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75d3bcfa90454dba8df12adc86b13b6d85fda97d90e708efc036c2760cc6ba44"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5691340f259b8f76b45fb31b98e594d46c36d1dc8285efa7975f7f50230c9093"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e263cc718545b7f897baeac1f00299ab6fabe3e18caaacacb0edf6d5f35513c"},
{file = "watchfiles-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6cf7709ed3e55704cc06f6e835bf43c03bc8e3cb8ff946bf69a2e0a78d9d77"},
{file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:703aa5e50e465be901e0e0f9d5739add15e696d8c26c53bc6fc00eb65d7b9469"},
{file = "watchfiles-1.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bfcae6aecd9e0cb425f5145afee871465b98b75862e038d42fe91fd753ddd780"},
{file = "watchfiles-1.0.3-cp311-cp311-win32.whl", hash = "sha256:6a76494d2c5311584f22416c5a87c1e2cb954ff9b5f0988027bc4ef2a8a67181"},
{file = "watchfiles-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:cf745cbfad6389c0e331786e5fe9ae3f06e9d9c2ce2432378e1267954793975c"},
{file = "watchfiles-1.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:2dcc3f60c445f8ce14156854a072ceb36b83807ed803d37fdea2a50e898635d6"},
{file = "watchfiles-1.0.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:93436ed550e429da007fbafb723e0769f25bae178fbb287a94cb4ccdf42d3af3"},
{file = "watchfiles-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c18f3502ad0737813c7dad70e3e1cc966cc147fbaeef47a09463bbffe70b0a00"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5bc3ca468bb58a2ef50441f953e1f77b9a61bd1b8c347c8223403dc9b4ac9a"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d1ec043f02ca04bf21b1b32cab155ce90c651aaf5540db8eb8ad7f7e645cba8"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f58d3bfafecf3d81c15d99fc0ecf4319e80ac712c77cf0ce2661c8cf8bf84066"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1df924ba82ae9e77340101c28d56cbaff2c991bd6fe8444a545d24075abb0a87"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:632a52dcaee44792d0965c17bdfe5dc0edad5b86d6a29e53d6ad4bf92dc0ff49"},
{file = "watchfiles-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bf4b459d94a0387617a1b499f314aa04d8a64b7a0747d15d425b8c8b151da0"},
{file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca94c85911601b097d53caeeec30201736ad69a93f30d15672b967558df02885"},
{file = "watchfiles-1.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65ab1fb635476f6170b07e8e21db0424de94877e4b76b7feabfe11f9a5fc12b5"},
{file = "watchfiles-1.0.3-cp312-cp312-win32.whl", hash = "sha256:49bc1bc26abf4f32e132652f4b3bfeec77d8f8f62f57652703ef127e85a3e38d"},
{file = "watchfiles-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:48681c86f2cb08348631fed788a116c89c787fdf1e6381c5febafd782f6c3b44"},
{file = "watchfiles-1.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:9e080cf917b35b20c889225a13f290f2716748362f6071b859b60b8847a6aa43"},
{file = "watchfiles-1.0.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e153a690b7255c5ced17895394b4f109d5dcc2a4f35cb809374da50f0e5c456a"},
{file = "watchfiles-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac1be85fe43b4bf9a251978ce5c3bb30e1ada9784290441f5423a28633a958a7"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec98e31e1844eac860e70d9247db9d75440fc8f5f679c37d01914568d18721"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0179252846be03fa97d4d5f8233d1c620ef004855f0717712ae1c558f1974a16"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995c374e86fa82126c03c5b4630c4e312327ecfe27761accb25b5e1d7ab50ec8"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b9cb35b7f290db1c31fb2fdf8fc6d3730cfa4bca4b49761083307f441cac5a"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f8dc09ae69af50bead60783180f656ad96bd33ffbf6e7a6fce900f6d53b08f1"},
{file = "watchfiles-1.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489b80812f52a8d8c7b0d10f0d956db0efed25df2821c7a934f6143f76938bd6"},
{file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:228e2247de583475d4cebf6b9af5dc9918abb99d1ef5ee737155bb39fb33f3c0"},
{file = "watchfiles-1.0.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1550be1a5cb3be08a3fb84636eaafa9b7119b70c71b0bed48726fd1d5aa9b868"},
{file = "watchfiles-1.0.3-cp313-cp313-win32.whl", hash = "sha256:16db2d7e12f94818cbf16d4c8938e4d8aaecee23826344addfaaa671a1527b07"},
{file = "watchfiles-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:160eff7d1267d7b025e983ca8460e8cc67b328284967cbe29c05f3c3163711a3"},
{file = "watchfiles-1.0.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c05b021f7b5aa333124f2a64d56e4cb9963b6efdf44e8d819152237bbd93ba15"},
{file = "watchfiles-1.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:310505ad305e30cb6c5f55945858cdbe0eb297fc57378f29bacceb534ac34199"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddff3f8b9fa24a60527c137c852d0d9a7da2a02cf2151650029fdc97c852c974"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46e86ed457c3486080a72bc837300dd200e18d08183f12b6ca63475ab64ed651"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f79fe7993e230a12172ce7d7c7db061f046f672f2b946431c81aff8f60b2758b"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea2b51c5f38bad812da2ec0cd7eec09d25f521a8b6b6843cbccedd9a1d8a5c15"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fe4e740ea94978b2b2ab308cbf9270a246bcbb44401f77cc8740348cbaeac3d"},
{file = "watchfiles-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af037d3df7188ae21dc1c7624501f2f90d81be6550904e07869d8d0e6766655"},
{file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52bb50a4c4ca2a689fdba84ba8ecc6a4e6210f03b6af93181bb61c4ec3abaf86"},
{file = "watchfiles-1.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c14a07bdb475eb696f85c715dbd0f037918ccbb5248290448488a0b4ef201aad"},
{file = "watchfiles-1.0.3-cp39-cp39-win32.whl", hash = "sha256:be37f9b1f8934cd9e7eccfcb5612af9fb728fecbe16248b082b709a9d1b348bf"},
{file = "watchfiles-1.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ef9ec8068cf23458dbf36a08e0c16f0a2df04b42a8827619646637be1769300a"},
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:84fac88278f42d61c519a6c75fb5296fd56710b05bbdcc74bdf85db409a03780"},
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c68be72b1666d93b266714f2d4092d78dc53bd11cf91ed5a3c16527587a52e29"},
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889a37e2acf43c377b5124166bece139b4c731b61492ab22e64d371cce0e6e80"},
{file = "watchfiles-1.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca05cacf2e5c4a97d02a2878a24020daca21dbb8823b023b978210a75c79098"},
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8af4b582d5fc1b8465d1d2483e5e7b880cc1a4e99f6ff65c23d64d070867ac58"},
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:127de3883bdb29dbd3b21f63126bb8fa6e773b74eaef46521025a9ce390e1073"},
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713f67132346bdcb4c12df185c30cf04bdf4bf6ea3acbc3ace0912cab6b7cb8c"},
{file = "watchfiles-1.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd85de513eb83f5ec153a802348e7a5baa4588b818043848247e3e8986094e8"},
{file = "watchfiles-1.0.3.tar.gz", hash = "sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56"},
]
[package.dependencies]
@@ -1561,4 +1549,4 @@ inmem = ["langgraph-api", "python-dotenv"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "8eaaa66d9e6e447699e3bcee336dfe779b58c956f8c2ad6678008a07be935838"
content-hash = "c36514f708e8b2b32c20ad9b7a3ba6fca41f043ced0149f0aae4eef43fc6c10b"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.61"
version = "0.1.64"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
langgraph-api = { version = ">=0.0.6,<0.1.0", optional = true, python = ">=3.11,<4.0" }
langgraph-api = { version = ">=0.0.8,<0.1.0", optional = true, python = ">=3.11,<4.0" }
python-dotenv = { version = ">=0.8.0", optional = true }
[tool.poetry.group.dev.dependencies]
+2
View File
@@ -31,6 +31,7 @@ def test_validate_config():
"dockerfile_lines": [],
"env": {},
"store": None,
"auth": None,
**expected_config,
}
actual_config = validate_config(expected_config)
@@ -48,6 +49,7 @@ def test_validate_config():
},
"env": env,
"store": None,
"auth": None,
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
+2 -2
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Generic, Optional, Sequence, Type, TypeVar
from typing import Any, Generic, Optional, Sequence, TypeVar
from typing_extensions import Self
@@ -13,7 +13,7 @@ C = TypeVar("C")
class BaseChannel(Generic[Value, Update, C], ABC):
__slots__ = ("key", "typ")
def __init__(self, typ: Type[Any], key: str = "") -> None:
def __init__(self, typ: Any, key: str = "") -> None:
self.typ = typ
self.key = key
+4
View File
@@ -40,12 +40,16 @@ SCHEDULED = sys.intern("__scheduled__")
# marker to signal node was scheduled (in distributed mode)
TASKS = sys.intern("__pregel_tasks")
# for Send objects returned by nodes/edges, corresponds to PUSH below
RETURN = sys.intern("__return__")
# for writes of a task where we simply record the return value
# --- Reserved config.configurable keys ---
CONFIG_KEY_SEND = sys.intern("__pregel_send")
# holds the `write` function that accepts writes to state/edges/reserved keys
CONFIG_KEY_READ = sys.intern("__pregel_read")
# holds the `read` function that returns a copy of the current state
CONFIG_KEY_CALL = sys.intern("__pregel_call")
# holds the `call` function that accepts a node/func, args and returns a future
CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer")
# holds a `BaseCheckpointSaver` passed from parent graph to child graphs
CONFIG_KEY_STREAM = sys.intern("__pregel_stream")
+120
View File
@@ -0,0 +1,120 @@
import asyncio
import concurrent
import concurrent.futures
import inspect
import types
from functools import partial, update_wrapper
from typing import (
Any,
Awaitable,
Callable,
Optional,
TypeVar,
Union,
overload,
)
from typing_extensions import ParamSpec
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import END, START, TAG_HIDDEN
from langgraph.pregel import Pregel
from langgraph.pregel.call import get_runnable_for_func
from langgraph.pregel.read import PregelNode
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.store.base import BaseStore
from langgraph.types import RetryPolicy, StreamMode, StreamWriter
P = ParamSpec("P")
P1 = TypeVar("P1")
T = TypeVar("T")
def call(
func: Callable[[P1], T],
input: P1,
*,
retry: Optional[RetryPolicy] = None,
) -> concurrent.futures.Future[T]:
from langgraph.constants import CONFIG_KEY_CALL
from langgraph.utils.config import get_configurable
conf = get_configurable()
impl = conf[CONFIG_KEY_CALL]
fut = impl(func, input, retry=retry)
return fut
@overload
def task(
*, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]]: ...
@overload
def task( # type: ignore[overload-cannot-match]
*, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]]: ...
def task(
*, retry: Optional[RetryPolicy] = None
) -> Union[
Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]],
Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]],
]:
def _task(func: Callable[P, T]) -> Callable[P, concurrent.futures.Future[T]]:
return update_wrapper(partial(call, func, retry=retry), func)
return _task
def entrypoint(
*,
checkpointer: Optional[BaseCheckpointSaver] = None,
store: Optional[BaseStore] = None,
) -> Callable[[types.FunctionType], Pregel]:
def _imp(func: types.FunctionType) -> Pregel:
if inspect.isgeneratorfunction(func):
def gen_wrapper(*args: Any, writer: StreamWriter, **kwargs: Any) -> Any:
for chunk in func(*args, **kwargs):
writer(chunk)
bound = get_runnable_for_func(gen_wrapper)
stream_mode: StreamMode = "custom"
elif inspect.isasyncgenfunction(func):
async def agen_wrapper(
*args: Any, writer: StreamWriter, **kwargs: Any
) -> Any:
async for chunk in func(*args, **kwargs):
writer(chunk)
bound = get_runnable_for_func(agen_wrapper)
stream_mode = "custom"
else:
bound = get_runnable_for_func(func)
stream_mode = "updates"
return Pregel(
nodes={
func.__name__: PregelNode(
bound=bound,
triggers=[START],
channels=[START],
writers=[ChannelWrite([ChannelWriteEntry(END)], tags=[TAG_HIDDEN])],
)
},
channels={START: EphemeralValue(Any), END: LastValue(Any, END)},
input_channels=START,
output_channels=END,
stream_channels=END,
stream_mode=stream_mode,
checkpointer=checkpointer,
store=store,
)
return _imp
+23 -16
View File
@@ -559,6 +559,7 @@ class StateGraph(Graph):
for key, node in self.nodes.items():
compiled.attach_node(key, node)
compiled.attach_branch(START, SELF, CONTROL_BRANCH, with_reader=False)
for key, node in self.nodes.items():
compiled.attach_branch(key, SELF, CONTROL_BRANCH, with_reader=False)
@@ -613,21 +614,24 @@ class CompiledStateGraph(CompiledGraph):
]
def _get_root(input: Any) -> Optional[Sequence[tuple[str, Any]]]:
if (
isinstance(input, (list, tuple))
and input
and all(isinstance(i, Command) for i in input)
):
updates: list[tuple[str, Any]] = []
for i in input:
if i.graph == Command.PARENT:
continue
updates.extend(i._update_as_tuples())
return updates
elif isinstance(input, Command):
if isinstance(input, Command):
if input.graph == Command.PARENT:
return ()
return input._update_as_tuples()
elif (
isinstance(input, (list, tuple))
and input
and any(isinstance(i, Command) for i in input)
):
updates: list[tuple[str, Any]] = []
for i in input:
if isinstance(i, Command):
if i.graph == Command.PARENT:
continue
updates.extend(i._update_as_tuples())
else:
updates.append(("__root__", i))
return updates
elif input is not None:
return [("__root__", input)]
@@ -645,13 +649,16 @@ class CompiledStateGraph(CompiledGraph):
elif (
isinstance(input, (list, tuple))
and input
and all(isinstance(i, Command) for i in input)
and any(isinstance(i, Command) for i in input)
):
updates: list[tuple[str, Any]] = []
for i in input:
if i.graph == Command.PARENT:
continue
updates.extend(i._update_as_tuples())
if isinstance(i, Command):
if i.graph == Command.PARENT:
continue
updates.extend(i._update_as_tuples())
else:
updates.extend(_get_updates(i) or ())
return updates
elif get_type_hints(type(input)):
return [
+181 -62
View File
@@ -1,7 +1,7 @@
import asyncio
import inspect
import json
from copy import copy
from copy import copy, deepcopy
from typing import (
Any,
Callable,
@@ -20,6 +20,7 @@ from langchain_core.messages import (
AnyMessage,
ToolCall,
ToolMessage,
convert_to_messages,
)
from langchain_core.runnables import RunnableConfig
from langchain_core.runnables.config import (
@@ -35,6 +36,7 @@ from typing_extensions import Annotated, get_args, get_origin
from langgraph.errors import GraphBubbleUp
from langgraph.store.base import BaseStore
from langgraph.types import Command
from langgraph.utils.runnable import RunnableCallable
INVALID_TOOL_NAME_ERROR_TEMPLATE = (
@@ -47,7 +49,7 @@ def msg_content_output(output: Any) -> Union[str, list[dict]]:
recognized_content_block_types = ("image", "image_url", "text", "json")
if isinstance(output, str):
return output
elif all(
elif isinstance(output, list) and all(
[
isinstance(x, dict) and x.get("type") in recognized_content_block_types
for x in output
@@ -210,12 +212,31 @@ class ToolNode(RunnableCallable):
*,
store: BaseStore,
) -> Any:
tool_calls, output_type = self._parse_input(input, store)
tool_calls, input_type = self._parse_input(input, store)
config_list = get_config_list(config, len(tool_calls))
input_types = [input_type] * len(tool_calls)
with get_executor_for_config(config) as executor:
outputs = [*executor.map(self._run_one, tool_calls, config_list)]
# TypedDict, pydantic, dataclass, etc. should all be able to load from dict
return outputs if output_type == "list" else {self.messages_key: outputs}
outputs = [
*executor.map(self._run_one, tool_calls, input_types, config_list)
]
# preserve existing behavior for non-command tool outputs for backwards compatibility
if not any(isinstance(output, Command) for output in outputs):
# TypedDict, pydantic, dataclass, etc. should all be able to load from dict
return outputs if input_type == "list" else {self.messages_key: outputs}
# LangGraph will automatically handle list of Command and non-command node updates
combined_outputs: list[
Command | list[ToolMessage] | dict[str, list[ToolMessage]]
] = []
for output in outputs:
if isinstance(output, Command):
combined_outputs.append(output)
else:
combined_outputs.append(
[output] if input_type == "list" else {self.messages_key: [output]}
)
return combined_outputs
def invoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
@@ -242,26 +263,97 @@ class ToolNode(RunnableCallable):
*,
store: BaseStore,
) -> Any:
tool_calls, output_type = self._parse_input(input, store)
tool_calls, input_type = self._parse_input(input, store)
outputs = await asyncio.gather(
*(self._arun_one(call, config) for call in tool_calls)
*(self._arun_one(call, input_type, config) for call in tool_calls)
)
# TypedDict, pydantic, dataclass, etc. should all be able to load from dict
return outputs if output_type == "list" else {self.messages_key: outputs}
def _run_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage:
# preserve existing behavior for non-command tool outputs for backwards compatibility
if not any(isinstance(output, Command) for output in outputs):
# TypedDict, pydantic, dataclass, etc. should all be able to load from dict
return outputs if input_type == "list" else {self.messages_key: outputs}
# LangGraph will automatically handle list of Command and non-command node updates
combined_outputs: list[
Command | list[ToolMessage] | dict[str, list[ToolMessage]]
] = []
for output in outputs:
if isinstance(output, Command):
combined_outputs.append(output)
else:
combined_outputs.append(
[output] if input_type == "list" else {self.messages_key: [output]}
)
return combined_outputs
def _run_one(
self,
call: ToolCall,
input_type: Literal["list", "dict"],
config: RunnableConfig,
) -> ToolMessage:
if invalid_tool_message := self._validate_tool_call(call):
return invalid_tool_message
try:
input = {**call, **{"type": "tool_call"}}
tool_message: ToolMessage = self.tools_by_name[call["name"]].invoke(
input, config
response = self.tools_by_name[call["name"]].invoke(input, config)
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
handled_types: tuple = self.handle_tool_errors
elif callable(self.handle_tool_errors):
handled_types = _infer_handled_types(self.handle_tool_errors)
else:
# default behavior is catching all exceptions
handled_types = (Exception,)
# Unhandled
if not self.handle_tool_errors or not isinstance(e, handled_types):
raise e
# Handled
else:
content = _handle_tool_error(e, flag=self.handle_tool_errors)
return ToolMessage(
content=content,
name=call["name"],
tool_call_id=call["id"],
status="error",
)
tool_message.content = cast(
Union[str, list], msg_content_output(tool_message.content)
if isinstance(response, Command):
return self._validate_tool_command(response, call, input_type)
elif isinstance(response, ToolMessage):
response.content = cast(
Union[str, list], msg_content_output(response.content)
)
return tool_message
return response
else:
raise TypeError(
f"Tool {call['name']} returned unexpected type: {type(response)}"
)
async def _arun_one(
self,
call: ToolCall,
input_type: Literal["list", "dict"],
config: RunnableConfig,
) -> ToolMessage:
if invalid_tool_message := self._validate_tool_call(call):
return invalid_tool_message
try:
input = {**call, **{"type": "tool_call"}}
response = await self.tools_by_name[call["name"]].ainvoke(input, config)
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
@@ -286,50 +378,24 @@ class ToolNode(RunnableCallable):
else:
content = _handle_tool_error(e, flag=self.handle_tool_errors)
return ToolMessage(
content=content, name=call["name"], tool_call_id=call["id"], status="error"
)
async def _arun_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage:
if invalid_tool_message := self._validate_tool_call(call):
return invalid_tool_message
try:
input = {**call, **{"type": "tool_call"}}
tool_message: ToolMessage = await self.tools_by_name[call["name"]].ainvoke(
input, config
return ToolMessage(
content=content,
name=call["name"],
tool_call_id=call["id"],
status="error",
)
tool_message.content = cast(
Union[str, list], msg_content_output(tool_message.content)
if isinstance(response, Command):
return self._validate_tool_command(response, call, input_type)
elif isinstance(response, ToolMessage):
response.content = cast(
Union[str, list], msg_content_output(response.content)
)
return response
else:
raise TypeError(
f"Tool {call['name']} returned unexpected type: {type(response)}"
)
return tool_message
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
handled_types: tuple = self.handle_tool_errors
elif callable(self.handle_tool_errors):
handled_types = _infer_handled_types(self.handle_tool_errors)
else:
# default behavior is catching all exceptions
handled_types = (Exception,)
# Unhandled
if not self.handle_tool_errors or not isinstance(e, handled_types):
raise e
# Handled
else:
content = _handle_tool_error(e, flag=self.handle_tool_errors)
return ToolMessage(
content=content, name=call["name"], tool_call_id=call["id"], status="error"
)
def _parse_input(
self,
@@ -341,14 +407,14 @@ class ToolNode(RunnableCallable):
store: BaseStore,
) -> Tuple[list[ToolCall], Literal["list", "dict"]]:
if isinstance(input, list):
output_type = "list"
input_type = "list"
message: AnyMessage = input[-1]
elif isinstance(input, dict) and (messages := input.get(self.messages_key, [])):
output_type = "dict"
input_type = "dict"
message = messages[-1]
elif messages := getattr(input, self.messages_key, None):
# Assume dataclass-like state that can coerce from dict
output_type = "dict"
input_type = "dict"
message = messages[-1]
else:
raise ValueError("No message found in input")
@@ -359,7 +425,7 @@ class ToolNode(RunnableCallable):
tool_calls = [
self._inject_tool_args(call, input, store) for call in message.tool_calls
]
return tool_calls, output_type
return tool_calls, input_type
def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]:
if (requested_tool := call["name"]) not in self.tools_by_name:
@@ -453,6 +519,59 @@ class ToolNode(RunnableCallable):
tool_call_with_store = self._inject_store(tool_call_with_state, store)
return tool_call_with_store
def _validate_tool_command(
self, command: Command, call: ToolCall, input_type: Literal["list", "dict"]
) -> Command:
if isinstance(command.update, dict):
# input type is dict when ToolNode is invoked with a dict input (e.g. {"messages": [AIMessage(..., tool_calls=[...])]})
if input_type != "dict":
raise ValueError(
f"Tools can provide a dict in Command.update only when using dict with '{self.messages_key}' key as ToolNode input, "
f"got: {command.update} for tool '{call['name']}'"
)
updated_command = deepcopy(command)
state_update = cast(dict[str, Any], updated_command.update) or {}
messages_update = state_update.get(self.messages_key, [])
elif isinstance(command.update, list):
# input type is list when ToolNode is invoked with a list input (e.g. [AIMessage(..., tool_calls=[...])])
if input_type != "list":
raise ValueError(
f"Tools can provide a list of messages in Command.update only when using list of messages as ToolNode input, "
f"got: {command.update} for tool '{call['name']}'"
)
updated_command = deepcopy(command)
messages_update = updated_command.update
else:
return command
# convert to message objects if updates are in a dict format
messages_update = convert_to_messages(messages_update)
has_matching_tool_message = False
for message in messages_update:
if not isinstance(message, ToolMessage):
continue
if message.tool_call_id == call["id"]:
message.name = call["name"]
has_matching_tool_message = True
# validate that we always have a ToolMessage matching the tool call in
# Command.update if command is sent to the CURRENT graph
if updated_command.graph is None and not has_matching_tool_message:
example_update = (
'`Command(update={"messages": [ToolMessage("Success", tool_call_id=tool_call_id), ...]}, ...)`'
if input_type == "dict"
else '`Command(update=[ToolMessage("Success", tool_call_id=tool_call_id), ...], ...)`'
)
raise ValueError(
f"Expected to have a matching ToolMessage in Command.update for tool '{call['name']}', got: {messages_update}. "
"Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage. "
f"You can fix it by modifying the tool to return {example_update}."
)
return updated_command
def tools_condition(
state: Union[list[AnyMessage], dict[str, Any], BaseModel],
+10 -3
View File
@@ -18,7 +18,6 @@ from typing import (
Type,
Union,
cast,
get_type_hints,
overload,
)
from uuid import UUID, uuid5
@@ -117,6 +116,7 @@ from langgraph.utils.config import (
patch_config,
patch_configurable,
)
from langgraph.utils.fields import get_enhanced_type_hints
from langgraph.utils.pydantic import create_model
from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined]
@@ -319,8 +319,15 @@ class Pregel(PregelProtocol):
)
+ (
[
ConfigurableFieldSpec(id=name, annotation=typ)
for name, typ in get_type_hints(self.config_type).items()
ConfigurableFieldSpec(
id=name,
annotation=typ,
default=default,
description=description,
)
for name, typ, default, description in get_enhanced_type_hints(
self.config_type
)
]
if self.config_type is not None
else []
+134 -20
View File
@@ -43,6 +43,7 @@ from langgraph.constants import (
CONFIG_KEY_TASK_ID,
CONFIG_KEY_WRITES,
EMPTY_SEQ,
ERROR,
INTERRUPT,
NO_WRITES,
NS_END,
@@ -52,18 +53,26 @@ from langgraph.constants import (
PUSH,
RESERVED,
RESUME,
RETURN,
TAG_HIDDEN,
TASKS,
Send,
)
from langgraph.errors import EmptyChannelError, InvalidUpdateError
from langgraph.managed.base import ManagedValueMapping
from langgraph.pregel.call import get_runnable_for_func
from langgraph.pregel.io import read_channel, read_channels
from langgraph.pregel.log import logger
from langgraph.pregel.manager import ChannelsManager
from langgraph.pregel.read import PregelNode
from langgraph.store.base import BaseStore
from langgraph.types import All, LoopProtocol, PregelExecutableTask, PregelTask
from langgraph.types import (
All,
LoopProtocol,
PregelExecutableTask,
PregelTask,
RetryPolicy,
)
from langgraph.utils.config import merge_configs, patch_config
GetNextVersion = Callable[[Optional[V], BaseChannel], V]
@@ -97,6 +106,21 @@ class PregelTaskWrites(NamedTuple):
triggers: Sequence[str]
class Call:
__slots__ = ("func", "input", "retry")
func: Callable
input: Any
retry: Optional[RetryPolicy]
def __init__(
self, func: Callable, input: Any, *, retry: Optional[RetryPolicy]
) -> None:
self.func = func
self.input = input
self.retry = retry
def should_interrupt(
checkpoint: Checkpoint,
interrupt_nodes: Union[All, Sequence[str]],
@@ -179,7 +203,7 @@ def local_write(
"""Function injected under CONFIG_KEY_SEND in task config, to write to channels.
Validates writes and forwards them to `commit` function."""
for chan, value in writes:
if chan in (PUSH, TASKS):
if chan in (PUSH, TASKS) and value is not None:
if not isinstance(value, Send):
raise InvalidUpdateError(f"Expected Send, got {value}")
if value.node not in process_keys:
@@ -247,7 +271,7 @@ def apply_writes(
pending_writes_by_managed: dict[str, list[Any]] = defaultdict(list)
for task in tasks:
for chan, val in task.writes:
if chan in (NO_WRITES, PUSH, RESUME, INTERRUPT):
if chan in (NO_WRITES, PUSH, RESUME, INTERRUPT, RETURN, ERROR):
pass
elif chan == TASKS: # TODO: remove branch in 1.0
checkpoint["pending_sends"].append(val)
@@ -438,7 +462,7 @@ def prepare_next_tasks(
def prepare_single_task(
task_path: tuple[Union[str, int, tuple], ...],
task_path: tuple[Any, ...],
task_id_checksum: Optional[str],
*,
checkpoint: Checkpoint,
@@ -459,7 +483,94 @@ def prepare_single_task(
configurable = config.get(CONF, {})
parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
if task_path[0] == PUSH:
if task_path[0] == PUSH and isinstance(task_path[-1], Call):
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_path_t = cast(tuple[str, tuple, int, str, Call], task_path)
call = task_path_t[-1]
proc_ = get_runnable_for_func(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers = [PUSH]
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = _uuid5_str(
checkpoint_id,
checkpoint_ns,
str(step),
name,
PUSH,
_tuple_str(task_path[1]),
str(task_path[2]),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": task_path[:3],
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
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,
step,
checkpoint,
channels,
managed,
PregelTaskWrites(task_path[:3], name, writes, triggers),
config,
),
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_WRITES: [
w
for w in pending_writes
+ configurable.get(CONFIG_KEY_WRITES, [])
if w[0] in (NULL_TASK_ID, task_id)
],
CONFIG_KEY_SCRATCHPAD: {},
},
),
triggers,
call.retry,
None,
task_id,
task_path[:3],
)
else:
return PregelTask(task_id, name, task_path[:3])
elif task_path[0] == PUSH:
if len(task_path) == 2: # TODO: remove branch in 1.0
# legacy SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
@@ -490,17 +601,19 @@ def prepare_single_task(
PUSH,
str(idx),
)
elif len(task_path) == 4:
elif len(task_path) >= 4:
# new PUSH tasks, executed in superstep n
# (PUSH, parent task path, idx of PUSH write, id of parent task)
task_path_t = cast(tuple[str, tuple, int, str], task_path)
writes_for_path = [w for w in pending_writes if w[0] == task_path_t[3]]
if task_path_t[2] >= len(writes_for_path):
task_path_tt = cast(tuple[str, tuple, int, str], task_path)
writes_for_path = [w for w in pending_writes if w[0] == task_path_tt[3]]
if task_path_tt[2] >= len(writes_for_path):
logger.warning(
f"Ignoring invalid write index {task_path[2]} in pending writes"
)
return
packet = writes_for_path[task_path_t[2]][2]
packet = writes_for_path[task_path_tt[2]][2]
if packet is None:
return
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending writes"
@@ -533,7 +646,7 @@ def prepare_single_task(
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_path": task_path[:3],
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
@@ -543,7 +656,7 @@ def prepare_single_task(
if node := proc.node:
if proc.metadata:
metadata.update(proc.metadata)
writes: deque[tuple[str, Any]] = deque()
writes = deque()
return PregelExecutableTask(
packet.node,
packet.arg,
@@ -572,7 +685,7 @@ def prepare_single_task(
channels,
managed,
PregelTaskWrites(
task_path, packet.node, writes, triggers
task_path[:3], packet.node, writes, triggers
),
config,
),
@@ -602,12 +715,11 @@ def prepare_single_task(
proc.retry_policy,
None,
task_id,
task_path,
task_path[:3],
writers=proc.flat_writers,
)
else:
return PregelTask(task_id, packet.node, task_path)
return PregelTask(task_id, packet.node, task_path[:3])
elif task_path[0] == PULL:
# (PULL, node name)
name = cast(str, task_path[1])
@@ -657,7 +769,7 @@ def prepare_single_task(
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_path": task_path[:3],
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
@@ -696,7 +808,9 @@ def prepare_single_task(
checkpoint,
channels,
managed,
PregelTaskWrites(task_path, name, writes, triggers),
PregelTaskWrites(
task_path[:3], name, writes, triggers
),
config,
),
CONFIG_KEY_STORE: (
@@ -725,11 +839,11 @@ def prepare_single_task(
proc.retry_policy,
None,
task_id,
task_path,
task_path[:3],
writers=proc.flat_writers,
)
else:
return PregelTask(task_id, name, task_path)
return PregelTask(task_id, name, task_path[:3])
def _proc_input(
+123
View File
@@ -0,0 +1,123 @@
import sys
import types
from typing import Any, Callable, Optional
from langgraph.constants import RETURN
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.utils.runnable import RunnableSeq, coerce_to_runnable
"""
Utilities borrowed from cloudpickle.
https://github.com/cloudpipe/cloudpickle/blob/6220b0ce83ffee5e47e06770a1ee38ca9e47c850/cloudpickle/cloudpickle.py#L265
"""
def _getattribute(obj: Any, name: str) -> Any:
for subpath in name.split("."):
if subpath == "<locals>":
raise AttributeError(
"Can't get local attribute {!r} on {!r}".format(name, obj)
)
try:
parent = obj
obj = getattr(obj, subpath)
except AttributeError:
raise AttributeError(
"Can't get attribute {!r} on {!r}".format(name, obj)
) from None
return obj, parent
def _whichmodule(obj: Any, name: str) -> Optional[str]:
"""Find the module an object belongs to.
This function differs from ``pickle.whichmodule`` in two ways:
- it does not mangle the cases where obj's module is __main__ and obj was
not found in any module.
- Errors arising during module introspection are ignored, as those errors
are considered unwanted side effects.
"""
module_name = getattr(obj, "__module__", None)
if module_name is not None:
return module_name
# Protect the iteration by using a copy of sys.modules against dynamic
# modules that trigger imports of other modules upon calls to getattr or
# other threads importing at the same time.
for module_name, module in sys.modules.copy().items():
# Some modules such as coverage can inject non-module objects inside
# sys.modules
if (
module_name == "__main__"
or module_name == "__mp_main__"
or module is None
or not isinstance(module, types.ModuleType)
):
continue
try:
if _getattribute(module, name)[0] is obj:
return module_name
except Exception:
pass
return None
def _lookup_module_and_qualname(
obj: Any, name: Optional[str] = None
) -> Optional[tuple[types.ModuleType, str]]:
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 = _whichmodule(obj, name)
if module_name is None:
# In this case, obj.__module__ is None AND obj was not found in any
# imported module. obj is thus treated as dynamic.
return None
if module_name == "__main__":
return None
# Note: if module_name is in sys.modules, the corresponding module is
# assumed importable at unpickling time. See #357
module = sys.modules.get(module_name, None)
if module is None:
# The main reason why obj's module would not be imported is that this
# module has been dynamically created, using for example
# types.ModuleType. The other possibility is that module was removed
# from sys.modules after obj was created/imported. But this case is not
# supported, as the standard pickle does not support it either.
return None
try:
obj2, parent = _getattribute(module, name)
except AttributeError:
# obj was not found inside the module it points to
return None
if obj2 is not obj:
return None
return module, name
def get_runnable_for_func(func: Callable[..., Any]) -> RunnableSeq:
if func in CACHE:
return CACHE[func]
else:
seq = RunnableSeq(
coerce_to_runnable(func, name=None, trace=False),
ChannelWrite([ChannelWriteEntry(RETURN)]),
name=func.__name__,
)
if not _lookup_module_and_qualname(func):
return seq
return CACHE.setdefault(func, seq)
CACHE: dict[Callable[..., Any], RunnableSeq] = {}
+22 -1
View File
@@ -1,6 +1,7 @@
import asyncio
import concurrent.futures
import sys
import time
from contextlib import ExitStack
from contextvars import copy_context
from types import TracebackType
@@ -34,6 +35,7 @@ class Submit(Protocol[P, T]):
__name__: Optional[str] = None,
__cancel_on_exit__: bool = False,
__reraise_on_exit__: bool = True,
__next_tick__: bool = False,
**kwargs: P.kwargs,
) -> concurrent.futures.Future[T]: ...
@@ -58,9 +60,13 @@ class BackgroundExecutor(ContextManager):
__name__: Optional[str] = None, # currently not used in sync version
__cancel_on_exit__: bool = False, # for sync, can cancel only if not started
__reraise_on_exit__: bool = True,
__next_tick__: bool = False,
**kwargs: P.kwargs,
) -> concurrent.futures.Future[T]:
task = self.executor.submit(fn, *args, **kwargs)
if __next_tick__:
task = self.executor.submit(next_tick, fn, *args, **kwargs)
else:
task = self.executor.submit(fn, *args, **kwargs)
self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
task.add_done_callback(self.done)
return task
@@ -137,11 +143,14 @@ class AsyncBackgroundExecutor(AsyncContextManager):
__name__: Optional[str] = None,
__cancel_on_exit__: bool = False,
__reraise_on_exit__: bool = True,
__next_tick__: bool = False,
**kwargs: P.kwargs,
) -> asyncio.Task[T]:
coro = cast(Coroutine[None, None, T], fn(*args, **kwargs))
if self.semaphore:
coro = gated(self.semaphore, coro)
if __next_tick__:
coro = anext_tick(coro)
if self.context_not_supported:
task = self.loop.create_task(coro, name=__name__)
else:
@@ -197,3 +206,15 @@ async def gated(semaphore: asyncio.Semaphore, coro: Coroutine[None, None, T]) ->
"""A coroutine that waits for a semaphore before running another coroutine."""
async with semaphore:
return await coro
def next_tick(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
"""A function that yields control to other threads before running another function."""
time.sleep(0)
return fn(*args, **kwargs)
async def anext_tick(coro: Coroutine[None, None, T]) -> T:
"""A coroutine that yields control to event loop before running another coroutine."""
await asyncio.sleep(0)
return await coro
+23 -19
View File
@@ -13,6 +13,9 @@ from langgraph.constants import (
NULL_TASK_ID,
PUSH,
RESUME,
RETURN,
SELF,
START,
TAG_HIDDEN,
TASKS,
)
@@ -78,12 +81,14 @@ def map_command(
else:
sends = [cmd.goto]
for send in sends:
if not isinstance(send, Send):
if isinstance(send, Send):
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:{START}:{SELF}:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send, got {type(send).__name__}"
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
# TODO handle goto str for state graph
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
@@ -167,22 +172,21 @@ def map_output_updates(
]
if not output_tasks:
return
if isinstance(output_channels, str):
updated = (
(task.name, value)
for task, writes in output_tasks
for chan, value in writes
if chan == output_channels
)
else:
updated = (
(
task.name,
{chan: value for chan, value in writes if chan in output_channels},
updated: list[tuple[str, Any]] = []
for task, writes in output_tasks:
if rtn := next((value for chan, value in writes if chan == RETURN), None):
updated.append((task.name, rtn))
elif isinstance(output_channels, str):
updated.extend(
(task.name, value) for chan, value in writes if chan == output_channels
)
elif any(chan in output_channels for chan, _ in writes):
updated.append(
(
task.name,
{chan: value for chan, value in writes if chan in output_channels},
)
)
for task, writes in output_tasks
if any(chan in output_channels for chan, _ in writes)
)
grouped: dict[str, list[Any]] = {t.name: [] for t, _ in output_tasks}
for node, value in updated:
grouped[node].append(value)
+44 -41
View File
@@ -73,6 +73,7 @@ from langgraph.managed.base import (
WritableManagedValue,
)
from langgraph.pregel.algo import (
Call,
GetNextVersion,
PregelTaskWrites,
apply_writes,
@@ -289,16 +290,15 @@ class PregelLoop(LoopProtocol):
if self.checkpointer_put_writes is not None:
self.submit(
self.checkpointer_put_writes,
{
**self.checkpoint_config,
CONF: {
**self.checkpoint_config[CONF],
patch_configurable(
self.checkpoint_config,
{
CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
CONFIG_KEY_CHECKPOINT_NS, ""
),
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
},
},
),
writes,
task_id,
)
@@ -307,20 +307,19 @@ class PregelLoop(LoopProtocol):
self._output_writes(task_id, writes)
def accept_push(
self, task: PregelExecutableTask, write_idx: int
self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None
) -> Optional[PregelExecutableTask]:
"""Accept a PUSH from a task, potentially returning a new task to start."""
# don't start if an earlier PUSH has already triggered an interrupt
if self.to_interrupt:
return
# don't start if we should interrupt *after* the original task
if should_interrupt(self.checkpoint, self.interrupt_after, [task]):
if self.interrupt_after and should_interrupt(
self.checkpoint, self.interrupt_after, [task]
):
self.to_interrupt.append(task)
return
if pushed := cast(
Optional[PregelExecutableTask],
prepare_single_task(
(PUSH, task.path, write_idx, task.id),
(PUSH, task.path, write_idx, task.id, call),
None,
checkpoint=self.checkpoint,
pending_writes=[(task.id, *w) for w in task.writes],
@@ -336,7 +335,9 @@ class PregelLoop(LoopProtocol):
),
):
# don't start if we should interrupt *before* the new task
if should_interrupt(self.checkpoint, self.interrupt_before, [pushed]):
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, [pushed]
):
self.to_interrupt.append(pushed)
return
# produce debug output
@@ -349,9 +350,8 @@ class PregelLoop(LoopProtocol):
# match any pending writes to the new task
if self.skip_done_tasks:
self._match_writes({pushed.id: pushed})
# return the new task, to be started, if not run before
if not pushed.writes:
return pushed
# return the new task, to be started if not run before
return pushed
def tick(
self,
@@ -413,7 +413,7 @@ class PregelLoop(LoopProtocol):
}
)
# after execution, check if we should interrupt
if should_interrupt(
if self.interrupt_after and should_interrupt(
self.checkpoint, self.interrupt_after, self.tasks.values()
):
self.status = "interrupt_after"
@@ -426,18 +426,6 @@ class PregelLoop(LoopProtocol):
self.status = "out_of_steps"
return False
# apply NULL writes
if null_writes := [
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
]:
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
# prepare next tasks
self.tasks = prepare_next_tasks(
self.checkpoint,
@@ -497,7 +485,7 @@ class PregelLoop(LoopProtocol):
return self.tick(input_keys=input_keys)
# before execution, check if we should interrupt
if should_interrupt(
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, self.tasks.values()
):
self.status = "interrupt_before"
@@ -539,9 +527,35 @@ class PregelLoop(LoopProtocol):
# - receiving None input (outer graph) or RESUMING flag (subgraph)
configurable = self.config.get(CONF, {})
is_resuming = bool(self.checkpoint["channel_versions"]) and bool(
configurable.get(CONFIG_KEY_RESUMING, self.input is None)
configurable.get(
CONFIG_KEY_RESUMING,
self.input is None or isinstance(self.input, Command),
)
)
# map command to writes
if isinstance(self.input, Command):
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
# group writes by task ID
for tid, c, v in map_command(self.input, self.checkpoint_pending_writes):
writes[tid].append((c, v))
if not writes:
raise EmptyInputError("Received empty Command input")
# save writes
for tid, ws in writes.items():
self.put_writes(tid, ws)
# apply NULL writes
if null_writes := [
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
]:
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
# proceed past previous checkpoint
if is_resuming:
self.checkpoint["versions_seen"].setdefault(INTERRUPT, {})
@@ -553,17 +567,6 @@ class PregelLoop(LoopProtocol):
self._emit(
"values", map_output_values, self.output_keys, True, self.channels
)
# map command to writes
elif isinstance(self.input, Command):
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
# group writes by task ID
for tid, c, v in map_command(self.input, self.checkpoint_pending_writes):
writes[tid].append((c, v))
if not writes:
raise EmptyInputError("Received empty Command input")
# save writes
for tid, ws in writes.items():
self.put_writes(tid, ws)
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# TODO shouldn't these writes be passed to put_writes too?
+11 -19
View File
@@ -4,14 +4,12 @@ import random
import sys
import time
from dataclasses import replace
from functools import partial
from typing import Any, Callable, Optional, Sequence
from typing import Any, Optional, Sequence
from langgraph.constants import (
CONF,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SEND,
NS_SEP,
)
from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphBubbleUp, ParentCommand
@@ -25,25 +23,21 @@ SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
def run_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
writer: Optional[
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
] = None,
configurable: Optional[dict[str, Any]] = None,
) -> None:
"""Run a task with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
attempts = 0
config = task.config
if writer is not None:
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
if configurable is not None:
config = patch_configurable(config, configurable)
while True:
try:
# clear any writes from previous attempts
task.writes.clear()
# run the task
task.proc.invoke(task.input, config)
# if successful, end
break
return task.proc.invoke(task.input, config)
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
@@ -115,17 +109,15 @@ async def arun_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
stream: bool = False,
writer: Optional[
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
] = None,
configurable: Optional[dict[str, Any]] = None,
) -> None:
"""Run a task asynchronously with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
attempts = 0
config = task.config
if writer is not None:
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
if configurable is not None:
config = patch_configurable(config, configurable)
while True:
try:
# clear any writes from previous attempts
@@ -134,10 +126,10 @@ async def arun_with_retry(
if stream:
async for _ in task.proc.astream(task.input, config):
pass
# if successful, end
break
else:
await task.proc.ainvoke(task.input, config)
# if successful, end
break
return await task.proc.ainvoke(task.input, config)
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
+258 -91
View File
@@ -1,9 +1,12 @@
import asyncio
import concurrent.futures
import threading
import time
from functools import partial
from typing import (
Any,
AsyncIterator,
Awaitable,
Callable,
Iterable,
Iterator,
@@ -16,18 +19,22 @@ from typing import (
from langgraph.constants import (
CONF,
CONFIG_KEY_CALL,
CONFIG_KEY_SEND,
ERROR,
INTERRUPT,
NO_WRITES,
PUSH,
RESUME,
RETURN,
TAG_HIDDEN,
)
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, RetryPolicy
from langgraph.utils.future import chain_future
class PregelRunner:
@@ -41,7 +48,7 @@ class PregelRunner:
submit: Submit,
put_writes: Callable[[str, Sequence[tuple[str, Any]]], None],
schedule_task: Callable[
[PregelExecutableTask, int], Optional[PregelExecutableTask]
[PregelExecutableTask, int, Optional[Call]], Optional[PregelExecutableTask]
],
use_astream: bool = False,
node_finished: Optional[Callable[[str], None]] = None,
@@ -61,73 +68,143 @@ class PregelRunner:
retry_policy: Optional[RetryPolicy] = None,
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
) -> Iterator[None]:
locks: dict[str, threading.Lock] = {}
def writer(
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
) -> None:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
for idx, w in enumerate(task.writes):
# find the index for the newly inserted writes
if idx < prev_length:
continue
assert writes[idx - prev_length] is w
task: PregelExecutableTask,
writes: Sequence[tuple[str, Any]],
*,
calls: Optional[Sequence[Call]] = None,
) -> Sequence[Optional[concurrent.futures.Future]]:
if all(w[0] != PUSH for w in writes):
return task.config[CONF][CONFIG_KEY_SEND](writes)
if task.id not in locks:
locks[task.id] = threading.Lock()
with locks[task.id]:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
# confirm no other concurrent writes were added
assert len(task.writes) == prev_length + len(writes)
# schedule PUSH tasks, collect futures
rtn: dict[int, Optional[concurrent.futures.Future]] = {}
for idx, w in enumerate(writes, start=prev_length):
# bail if not a PUSH write
if w[0] != PUSH:
continue
# schedule the next task, if the callback returns one
if next_task := self.schedule_task(task, idx):
# if the parent task was retried,
# the next task might already be running
if any(
t == next_task.id for t in futures.values() if t is not None
if next_task := self.schedule_task(
task, idx, calls[idx - prev_length] if calls else None
):
if fut := next(
(
f
for f, t in futures.items()
if t is not None and t == next_task.id
),
None,
):
continue
# schedule the next task
futures[
self.submit(
# if the parent task was retried,
# the next task might already be running
rtn[idx - prev_length] = fut
elif next_task.writes:
# if it already ran, return the result
fut = concurrent.futures.Future()
if val := next(v for c, v in next_task.writes if c == RETURN):
fut.set_result(val)
elif exc := next(v for c, v in next_task.writes if c == ERROR):
fut.set_exception(
exc
if isinstance(exc, BaseException)
else Exception(exc)
)
else:
fut.set_result(None)
rtn[idx - prev_length] = fut
else:
# schedule the next task
fut = self.submit(
run_with_retry,
next_task,
retry_policy,
writer=writer,
configurable={
CONFIG_KEY_SEND: partial(writer, next_task),
CONFIG_KEY_CALL: partial(call, next_task),
},
__reraise_on_exit__=reraise,
# starting a new task in the next tick ensures
# updates from this tick are committed/streamed first
__next_tick__=True,
)
] = next_task
fut.add_done_callback(partial(self.commit, next_task))
futures[fut] = next_task
rtn[idx - prev_length] = fut
return [rtn.get(i) for i in range(len(writes))]
def call(
task: PregelExecutableTask,
func: Callable[[Any], Union[Awaitable[Any], Any]],
input: Any,
*,
retry: Optional[RetryPolicy] = None,
) -> concurrent.futures.Future[Any]:
(fut,) = writer(
task, [(PUSH, None)], calls=[Call(func, input, retry=retry)]
)
assert fut is not None, "writer did not return a future for call"
return fut
tasks = tuple(tasks)
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {}
done_futures: set[concurrent.futures.Future] = set()
# 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:
t = tasks[0]
try:
run_with_retry(t, retry_policy, writer=writer)
run_with_retry(
t,
retry_policy,
configurable={
CONFIG_KEY_SEND: partial(writer, t),
CONFIG_KEY_CALL: partial(call, t),
},
)
self.commit(t, None)
except Exception as exc:
self.commit(t, exc)
if reraise:
self.commit(t, None, exc)
if reraise and futures:
# will be re-raised after futures are done
fut: concurrent.futures.Future = concurrent.futures.Future()
fut.set_exception(exc)
done_futures.add(fut)
elif reraise:
raise
if not futures: # maybe `t` schuduled another task
return
# add waiter task if requested
if get_waiter is not None:
futures[get_waiter()] = None
# schedule tasks
for t in tasks:
if not t.writes:
fut = self.submit(
run_with_retry,
t,
retry_policy,
configurable={
CONFIG_KEY_SEND: partial(writer, t),
CONFIG_KEY_CALL: partial(call, t),
},
__reraise_on_exit__=reraise,
)
fut.add_done_callback(partial(self.commit, t))
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
for t in tasks:
if not t.writes:
futures[
self.submit(
run_with_retry,
t,
retry_policy,
writer=writer,
__reraise_on_exit__=reraise,
)
] = t
done_futures: set[concurrent.futures.Future] = set()
end_time = timeout + time.monotonic() if timeout else None
while len(futures) > (1 if get_waiter is not None else 0):
done, inflight = concurrent.futures.wait(
@@ -146,8 +223,6 @@ class PregelRunner:
else:
# store for panic check
done_futures.add(fut)
# task finished, commit writes
self.commit(task, _exception(fut))
else:
# remove references to loop vars
del fut, task
@@ -156,6 +231,10 @@ class PregelRunner:
break
# give control back to the caller
yield
# wait for pending done callbacks
# if a 2nd future finishes while `wait` is returning, it's possible
# that done callbacks for the 2nd future aren't called until next tick
time.sleep(0)
# panic on failure or timeout
_panic_or_proceed(
done_futures.union(f for f, t in futures.items() if t is not None),
@@ -171,48 +250,109 @@ class PregelRunner:
retry_policy: Optional[RetryPolicy] = None,
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
) -> AsyncIterator[None]:
locks: dict[str, threading.Lock] = {}
def writer(
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
) -> None:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
for idx, w in enumerate(task.writes):
# find the index for the newly inserted writes
if idx < prev_length:
continue
assert writes[idx - prev_length] is w
task: PregelExecutableTask,
writes: Sequence[tuple[str, Any]],
*,
calls: Optional[Sequence[Call]] = None,
) -> Sequence[Optional[asyncio.Future]]:
if all(w[0] != PUSH for w in writes):
return task.config[CONF][CONFIG_KEY_SEND](writes)
if task.id not in locks:
locks[task.id] = threading.Lock()
with locks[task.id]:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
# confirm no other concurrent writes were added
assert len(task.writes) == prev_length + len(writes)
# schedule PUSH tasks, collect futures
rtn: dict[int, Optional[asyncio.Future]] = {}
for idx, w in enumerate(writes, start=prev_length):
# bail if not a PUSH write
if w[0] != PUSH:
continue
# schedule the next task, if the callback returns one
if next_task := self.schedule_task(task, idx):
wcall = calls[idx - prev_length] if calls is not None else None
if next_task := self.schedule_task(task, idx, wcall):
# if the parent task was retried,
# the next task might already be running
if any(
t == next_task.id for t in futures.values() if t is not None
if fut := next(
(
f
for f, t in futures.items()
if t is not None and t == next_task.id
),
None,
):
continue
# schedule the next task
futures[
cast(
# if the parent task was retried,
# the next task might already be running
rtn[idx - prev_length] = fut
elif next_task.writes:
# if it already ran, return the result
fut = asyncio.Future()
if val := next(v for c, v in next_task.writes if c == RETURN):
fut.set_result(val)
elif exc := next(v for c, v in next_task.writes if c == ERROR):
fut.set_exception(
exc
if isinstance(exc, BaseException)
else Exception(exc)
)
else:
fut.set_result(None)
rtn[idx - prev_length] = fut
else:
# schedule the next task
fut = cast(
asyncio.Future,
self.submit(
arun_with_retry,
next_task,
retry_policy,
stream=self.use_astream,
writer=writer,
configurable={
CONFIG_KEY_SEND: partial(writer, next_task),
CONFIG_KEY_CALL: partial(call, next_task),
},
__name__=t.name,
__cancel_on_exit__=True,
__reraise_on_exit__=reraise,
# starting a new task in the next tick ensures
# updates from this tick are committed/streamed first
__next_tick__=True,
),
)
] = next_task
fut.add_done_callback(partial(self.commit, next_task))
futures[fut] = next_task
rtn[idx - prev_length] = fut
return [rtn.get(i) for i in range(len(writes))]
def call(
task: PregelExecutableTask,
func: Callable[[Any], Union[Awaitable[Any], Any]],
input: Any,
*,
retry: Optional[RetryPolicy] = None,
) -> Union[asyncio.Future[Any], concurrent.futures.Future[Any]]:
(fut,) = writer(
task, [(PUSH, None)], calls=[Call(func, input, retry=retry)]
)
assert fut is not None, "writer did not return a future for call"
if asyncio.iscoroutinefunction(func):
return fut
# adapted from asyncio.run_coroutine_threadsafe
sfut: concurrent.futures.Future = concurrent.futures.Future()
loop.call_soon_threadsafe(chain_future, fut, sfut)
return sfut
loop = asyncio.get_event_loop()
tasks = tuple(tasks)
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {}
done_futures: set[asyncio.Future] = set()
# give control back to the caller
yield
# fast path if single task with no waiter and no timeout
@@ -220,39 +360,53 @@ class PregelRunner:
t = tasks[0]
try:
await arun_with_retry(
t, retry_policy, stream=self.use_astream, writer=writer
t,
retry_policy,
stream=self.use_astream,
configurable={
CONFIG_KEY_SEND: partial(writer, t),
CONFIG_KEY_CALL: partial(call, t),
},
)
self.commit(t, None)
except Exception as exc:
self.commit(t, exc)
if reraise:
self.commit(t, None, exc)
if reraise and futures:
# will be re-raised after futures are done
fut: asyncio.Future = loop.create_future()
fut.set_exception(exc)
done_futures.add(fut)
elif reraise:
raise
if not futures: # maybe `t` schuduled another task
return
# add waiter task if requested
if get_waiter is not None:
futures[get_waiter()] = None
# schedule tasks
for t in tasks:
if not t.writes:
fut = cast(
asyncio.Future,
self.submit(
arun_with_retry,
t,
retry_policy,
stream=self.use_astream,
configurable={
CONFIG_KEY_SEND: partial(writer, t),
CONFIG_KEY_CALL: partial(call, t),
},
__name__=t.name,
__cancel_on_exit__=True,
__reraise_on_exit__=reraise,
),
)
fut.add_done_callback(partial(self.commit, t))
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
for t in tasks:
if not t.writes:
futures[
cast(
asyncio.Future,
self.submit(
arun_with_retry,
t,
retry_policy,
stream=self.use_astream,
writer=writer,
__name__=t.name,
__cancel_on_exit__=True,
__reraise_on_exit__=reraise,
),
)
] = t
done_futures: set[asyncio.Future] = set()
end_time = timeout + loop.time() if timeout else None
while len(futures) > (1 if get_waiter is not None else 0):
done, inflight = await asyncio.wait(
@@ -271,8 +425,6 @@ class PregelRunner:
else:
# store for panic check
done_futures.add(fut)
# task finished, commit writes
self.commit(task, _exception(fut))
else:
# remove references to loop vars
del fut, task
@@ -281,6 +433,10 @@ class PregelRunner:
break
# give control back to the caller
yield
# wait for pending done callbacks
# if a 2nd future finishes while `wait` is returning, it's possible
# that done callbacks for the 2nd future aren't called until next tick
await asyncio.sleep(0)
# cancel waiter task
for fut in futures:
fut.cancel()
@@ -292,9 +448,19 @@ class PregelRunner:
)
def commit(
self, task: PregelExecutableTask, exception: Optional[BaseException]
self,
task: PregelExecutableTask,
fut: Union[None, concurrent.futures.Future[Any], asyncio.Future[Any]],
exception: Optional[BaseException] = None,
) -> None:
if exception:
if fut is not None:
exception = _exception(fut)
if isinstance(exception, asyncio.CancelledError):
# for cancelled tasks, also save error in task,
# so loop can finish super-step
task.writes.append((ERROR, exception))
self.put_writes(task.id, task.writes)
elif exception:
if isinstance(exception, GraphInterrupt):
# save interrupt to checkpointer
if interrupts := [(INTERRUPT, i) for i in exception.args[0]]:
@@ -325,11 +491,12 @@ def _should_stop_others(
GraphInterrupts are not considered failures."""
for fut in done:
if fut.cancelled():
return True
if exc := fut.exception():
return not isinstance(exc, GraphBubbleUp)
else:
return False
continue
elif exc := fut.exception():
if not isinstance(exc, GraphBubbleUp):
return True
return False
def _exception(
@@ -355,7 +522,9 @@ def _panic_or_proceed(
done: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
for fut in futs:
if fut.done():
if fut.cancelled():
continue
elif fut.done():
done.add(fut)
else:
inflight.add(fut)
@@ -368,8 +537,6 @@ def _panic_or_proceed(
# raise the exception
if panic:
raise exc
else:
return
if inflight:
# if we got here means we timed out
while inflight:
+103 -2
View File
@@ -32,6 +32,14 @@ if TYPE_CHECKING:
from langgraph.store.base import BaseStore
try:
from langchain_core.messages.tool import ToolOutputMixin
except ImportError:
class ToolOutputMixin: # type: ignore[no-redef]
pass
All = Literal["*"]
"""Special value to indicate that graph should interrupt on all nodes."""
@@ -244,7 +252,7 @@ N = TypeVar("N", bound=Hashable)
@dataclasses.dataclass(**_DC_KWARGS)
class Command(Generic[N]):
class Command(Generic[N], ToolOutputMixin):
"""One or more commands to update the graph's state and send messages to nodes.
Args:
@@ -263,7 +271,7 @@ class Command(Generic[N]):
"""
graph: Optional[str] = None
update: Union[dict[str, Any], Sequence[tuple[str, Any]]] = ()
update: Any = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
goto: Union[Send, Sequence[Union[Send, str]], str] = ()
@@ -339,6 +347,99 @@ class PregelScratchpad(TypedDict, total=False):
def interrupt(value: Any) -> Any:
"""Interrupt the graph with a resumable exception from within a node.
The `interrupt` function enables human-in-the-loop workflows by pausing graph
execution and surfacing a value to the client. This value can communicate context
or request input required to resume execution.
In a given node, the first invocation of this function raises a `GraphInterrupt`
exception, halting execution. The provided `value` is included with the exception
and sent to the client executing the graph.
A client resuming the graph must use the [`Command`][langgraph.types.Command]
primitive to specify a value for the interrupt and continue execution.
The graph resumes from the start of the node, **re-executing** all logic.
If a node contains multiple `interrupt` calls, LangGraph matches resume values
to interrupts based on their order in the node. This list of resume values
is scoped to the specific task executing the node and is not shared across tasks.
To use an `interrupt`, you must enable a checkpointer, as the feature relies
on persisting the graph state.
Example:
```python
import uuid
from typing import TypedDict, Optional
from langgraph.checkpoint.memory import MemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import interrupt
class State(TypedDict):
\"\"\"The graph state.\"\"\"
foo: str
human_value: Optional[str]
\"\"\"Human value will be updated using an interrupt.\"\"\"
def node(state: State):
answer = interrupt(
# This value will be sent to the client
# as part of the interrupt information.
\"what is your age?\"
)
print(f\"> Received an input from the interrupt: {answer}\")
return {\"human_value\": answer}
builder = StateGraph(State)
builder.add_node(\"node\", node)
builder.add_edge(START, \"node\")
# A checkpointer must be enabled for interrupts to work!
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
\"configurable\": {
\"thread_id\": uuid.uuid4(),
}
}
for chunk in graph.stream({\"foo\": \"abc\"}, config):
print(chunk)
```
```pycon
{'__interrupt__': (Interrupt(value='what is your age?', resumable=True, ns=['node:62e598fa-8653-9d6d-2046-a70203020e37'], when='during'),)}
```
```python
command = Command(resume=\"some input from a human!!!\")
for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):
print(chunk)
```
```pycon
Received an input from the interrupt: some input from a human!!!
{'node': {'human_value': 'some input from a human!!!'}}
```
Args:
value: The value to surface to the client when the graph is interrupted.
Returns:
Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation
Raises:
GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client.
"""
from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_SCRATCHPAD,
+42 -1
View File
@@ -1,5 +1,5 @@
import dataclasses
from typing import Any, Optional, Type, Union
from typing import Any, Generator, Optional, Type, Union, get_type_hints
from typing_extensions import Annotated, NotRequired, ReadOnly, Required, get_origin
@@ -106,3 +106,44 @@ def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any:
if _is_optional_type(type_):
return None
return ...
def get_enhanced_type_hints(
type: Type[Any],
) -> Generator[tuple[str, Any, Any, Optional[str]], None, None]:
"""Attempt to extract default values and descriptions from provided type, used for config schema."""
for name, typ in get_type_hints(type).items():
default = None
description = None
# Pydantic models
try:
if hasattr(type, "__fields__") and name in type.__fields__:
field = type.__fields__[name]
if hasattr(field, "description") and field.description is not None:
description = field.description
if hasattr(field, "default") and field.default is not None:
default = field.default
if (
hasattr(default, "__class__")
and getattr(default.__class__, "__name__", "")
== "PydanticUndefinedType"
):
default = None
except (AttributeError, KeyError, TypeError):
pass
# TypedDict, dataclass
try:
if hasattr(type, "__dict__"):
type_dict = getattr(type, "__dict__")
if name in type_dict:
default = type_dict[name]
except (AttributeError, KeyError, TypeError):
pass
yield name, typ, default, description
+124
View File
@@ -0,0 +1,124 @@
import asyncio
import concurrent.futures
from typing import Union
AnyFuture = Union[asyncio.Future, concurrent.futures.Future]
def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop:
# Tries to call Future.get_loop() if it's available.
# Otherwise fallbacks to using the old '_loop' property.
try:
get_loop = fut.get_loop
except AttributeError:
pass
else:
return get_loop()
return fut._loop
def _convert_future_exc(exc: BaseException) -> BaseException:
exc_class = type(exc)
if exc_class is concurrent.futures.CancelledError:
return asyncio.CancelledError(*exc.args)
elif exc_class is concurrent.futures.TimeoutError:
return asyncio.TimeoutError(*exc.args)
elif exc_class is concurrent.futures.InvalidStateError:
return asyncio.InvalidStateError(*exc.args)
else:
return exc
def _set_concurrent_future_state(
concurrent: concurrent.futures.Future,
source: AnyFuture,
) -> None:
"""Copy state from a future to a concurrent.futures.Future."""
assert source.done()
if source.cancelled():
concurrent.cancel()
if not concurrent.set_running_or_notify_cancel():
return
exception = source.exception()
if exception is not None:
concurrent.set_exception(_convert_future_exc(exception))
else:
result = source.result()
concurrent.set_result(result)
def _copy_future_state(source: AnyFuture, dest: asyncio.Future) -> None:
"""Internal helper to copy state from another Future.
The other Future may be a concurrent.futures.Future.
"""
assert source.done()
if dest.cancelled():
return
assert not dest.done()
if source.cancelled():
dest.cancel()
else:
exception = source.exception()
if exception is not None:
dest.set_exception(_convert_future_exc(exception))
else:
result = source.result()
dest.set_result(result)
def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
"""Chain two futures so that when one completes, so does the other.
The result (or exception) of source will be copied to destination.
If destination is cancelled, source gets cancelled too.
Compatible with both asyncio.Future and concurrent.futures.Future.
"""
if not asyncio.isfuture(source) and not isinstance(
source, concurrent.futures.Future
):
raise TypeError("A future is required for source argument")
if not asyncio.isfuture(destination) and not isinstance(
destination, concurrent.futures.Future
):
raise TypeError("A future is required for destination argument")
source_loop = _get_loop(source) if asyncio.isfuture(source) else None
dest_loop = _get_loop(destination) if asyncio.isfuture(destination) else None
def _set_state(future: AnyFuture, other: AnyFuture) -> None:
if asyncio.isfuture(future):
_copy_future_state(other, future)
else:
_set_concurrent_future_state(future, other)
def _call_check_cancel(destination: AnyFuture) -> None:
if destination.cancelled():
if source_loop is None or source_loop is dest_loop:
source.cancel()
else:
source_loop.call_soon_threadsafe(source.cancel)
def _call_set_state(source: AnyFuture) -> None:
if destination.cancelled() and dest_loop is not None and dest_loop.is_closed():
return
if dest_loop is None or dest_loop is source_loop:
_set_state(destination, source)
else:
if dest_loop.is_closed():
return
dest_loop.call_soon_threadsafe(_set_state, destination, source)
destination.add_done_callback(_call_check_cancel)
source.add_done_callback(_call_set_state)
def chain_future(source: AnyFuture, destination: concurrent.futures.Future) -> None:
# adapted from asyncio.run_coroutine_threadsafe
try:
_chain_future(source, destination)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
if destination.set_running_or_notify_cancel():
destination.set_exception(exc)
raise
+4 -12
View File
@@ -404,12 +404,10 @@ class RunnableSeq(Runnable):
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
context = copy_context()
context.run(_set_config_context, config)
if i == 0:
input = context.run(step.invoke, input, config, **kwargs)
input = step.invoke(input, config, **kwargs)
else:
input = context.run(step.invoke, input, config)
input = step.invoke(input, config)
# finish the root run
except BaseException as e:
run_manager.on_chain_error(e)
@@ -443,16 +441,10 @@ class RunnableSeq(Runnable):
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
context = copy_context()
context.run(_set_config_context, config)
if i == 0:
coro = step.ainvoke(input, config, **kwargs)
input = await step.ainvoke(input, config, **kwargs)
else:
coro = step.ainvoke(input, config)
if ASYNCIO_ACCEPTS_CONTEXT:
input = await asyncio.create_task(coro, context=context)
else:
input = await asyncio.create_task(coro)
input = await step.ainvoke(input, config)
# finish the root run
except BaseException as e:
await run_manager.on_chain_error(e)
+6 -6
View File
@@ -1325,13 +1325,13 @@ files = [
[[package]]
name = "langchain-core"
version = "0.3.15"
version = "0.3.23"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "langchain_core-0.3.15-py3-none-any.whl", hash = "sha256:3d4ca6dbb8ed396a6ee061063832a2451b0ce8c345570f7b086ffa7288e4fa29"},
{file = "langchain_core-0.3.15.tar.gz", hash = "sha256:b1a29787a4ffb7ec2103b4e97d435287201da7809b369740dd1e32f176325aba"},
{file = "langchain_core-0.3.23-py3-none-any.whl", hash = "sha256:550c0b996990830fa6515a71a1192a8a0343367999afc36d4ede14222941e420"},
{file = "langchain_core-0.3.23.tar.gz", hash = "sha256:f9e175e3b82063cc3b160c2ca2b155832e1c6f915312e1204828f97d4aabf6e1"},
]
[package.dependencies]
@@ -1382,7 +1382,7 @@ url = "../checkpoint-duckdb"
[[package]]
name = "langgraph-checkpoint-postgres"
version = "2.0.7"
version = "2.0.8"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1418,7 +1418,7 @@ url = "../checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
version = "0.1.42"
version = "0.1.43"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3413,4 +3413,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.0,<4.0"
content-hash = "2df4d5d5e61917bdfff0ba430067a17662666eedee2858d841fa02e594cf69d0"
content-hash = "936530a5f00f329aeff2e6e921fe64480be317fad2c0a59cd54ea9018d089304"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.56"
version = "0.2.59"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
[tool.poetry.dependencies]
python = ">=3.9.0,<4.0"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
langgraph-checkpoint = "^2.0.4"
langgraph-sdk = "^0.1.42"
File diff suppressed because one or more lines are too long
@@ -1,230 +1,4 @@
# serializer version: 1
# name: test_branch_then.1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([__start__]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([__end__]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[duckdb]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[duckdb].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[memory]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[memory].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[postgres]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[postgres].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[postgres_pipe]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[postgres_pipe].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[postgres_pool]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[postgres_pool].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_branch_then[sqlite]
'''
graph TD;
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
'''
# ---
# name: test_branch_then[sqlite].1
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
tool_two_slow(tool_two_slow)
tool_two_fast(tool_two_fast)
finish(finish)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
finish --> __end__;
prepare -.-> tool_two_slow;
tool_two_slow --> finish;
prepare -.-> tool_two_fast;
tool_two_fast --> finish;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_conditional_entrypoint_graph
'{"title": "LangGraphInput"}'
# ---
+43
View File
@@ -0,0 +1,43 @@
from typing import Literal, Union
from pydantic import BaseModel
# define these objects to avoid importing langchain_core.agents
# and therefore avoid relying on core Pydantic version
class AgentAction(BaseModel):
tool: str
tool_input: Union[str, dict]
log: str
type: Literal["AgentAction"] = "AgentAction"
model_config = {
"json_schema_extra": {
"description": (
"""Represents a request to execute an action by an agent.
The action consists of the name of the tool to execute and the input to pass
to the tool. The log is used to pass along extra information about the action."""
)
}
}
class AgentFinish(BaseModel):
"""Final return value of an ActionAgent.
Agents return an AgentFinish when they have reached a stopping condition.
"""
return_values: dict
log: str
type: Literal["AgentFinish"] = "AgentFinish"
model_config = {
"json_schema_extra": {
"description": (
"""Final return value of an ActionAgent.
Agents return an AgentFinish when they have reached a stopping condition."""
)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+623 -2
View File
@@ -46,7 +46,7 @@ from langgraph.prebuilt import (
create_react_agent,
tools_condition,
)
from langgraph.prebuilt.chat_agent_executor import _validate_chat_history
from langgraph.prebuilt.chat_agent_executor import AgentState, _validate_chat_history
from langgraph.prebuilt.tool_node import (
TOOL_CALL_ERROR_TEMPLATE,
InjectedState,
@@ -56,7 +56,7 @@ from langgraph.prebuilt.tool_node import (
)
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
from langgraph.types import Interrupt
from langgraph.types import Command, Interrupt, interrupt
from tests.conftest import (
ALL_CHECKPOINTERS_ASYNC,
ALL_CHECKPOINTERS_SYNC,
@@ -988,6 +988,627 @@ def test_tool_node_node_interrupt():
assert task.interrupts == (Interrupt(value="foo", when="during"),)
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,
reason="Langchain core 0.3.0 or greater is required",
)
async def test_tool_node_command():
from langchain_core.tools.base import InjectedToolCallId
@dec_tool
def transfer_to_bob(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Transfer to Bob"""
return Command(
update={
"messages": [
ToolMessage(content="Transferred to Bob", tool_call_id=tool_call_id)
]
},
goto="bob",
graph=Command.PARENT,
)
@dec_tool
async def async_transfer_to_bob(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Transfer to Bob"""
return Command(
update={
"messages": [
ToolMessage(content="Transferred to Bob", tool_call_id=tool_call_id)
]
},
goto="bob",
graph=Command.PARENT,
)
class CustomToolSchema(BaseModel):
tool_call_id: Annotated[str, InjectedToolCallId]
class MyCustomTool(BaseTool):
def _run(*args: Any, **kwargs: Any):
return Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id=kwargs["tool_call_id"],
)
]
},
goto="bob",
graph=Command.PARENT,
)
async def _arun(*args: Any, **kwargs: Any):
return Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id=kwargs["tool_call_id"],
)
]
},
goto="bob",
graph=Command.PARENT,
)
custom_tool = MyCustomTool(
name="custom_transfer_to_bob",
description="Transfer to bob",
args_schema=CustomToolSchema,
)
async_custom_tool = MyCustomTool(
name="async_custom_transfer_to_bob",
description="Transfer to bob",
args_schema=CustomToolSchema,
)
# test mixing regular tools and tools returning commands
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
result = ToolNode([add, transfer_to_bob]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{"args": {"a": 1, "b": 2}, "id": "1", "name": "add"},
{"args": {}, "id": "2", "name": "transfer_to_bob"},
],
)
]
}
)
assert result == [
{
"messages": [
ToolMessage(
content="3",
tool_call_id="1",
name="add",
)
]
},
Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id="2",
name="transfer_to_bob",
)
]
},
goto="bob",
graph=Command.PARENT,
),
]
# test tools returning commands
# test sync tools
for tool in [transfer_to_bob, custom_tool]:
result = ToolNode([tool]).invoke(
{
"messages": [
AIMessage(
"", tool_calls=[{"args": {}, "id": "1", "name": tool.name}]
)
]
}
)
assert result == [
Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name=tool.name,
)
]
},
goto="bob",
graph=Command.PARENT,
)
]
# test async tools
for tool in [async_transfer_to_bob, async_custom_tool]:
result = await ToolNode([tool]).ainvoke(
{
"messages": [
AIMessage(
"", tool_calls=[{"args": {}, "id": "1", "name": tool.name}]
)
]
}
)
assert result == [
Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name=tool.name,
)
]
},
goto="bob",
graph=Command.PARENT,
)
]
# test multiple commands
result = ToolNode([transfer_to_bob, custom_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "transfer_to_bob"},
{"args": {}, "id": "2", "name": "custom_transfer_to_bob"},
],
)
]
}
)
assert result == [
Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name="transfer_to_bob",
)
]
},
goto="bob",
graph=Command.PARENT,
),
Command(
update={
"messages": [
ToolMessage(
content="Transferred to Bob",
tool_call_id="2",
name="custom_transfer_to_bob",
)
]
},
goto="bob",
graph=Command.PARENT,
),
]
# test validation (mismatch between input type and command.update type)
with pytest.raises(ValueError):
@dec_tool
def list_update_tool(tool_call_id: Annotated[str, InjectedToolCallId]):
"""My tool"""
return Command(
update=[ToolMessage(content="foo", tool_call_id=tool_call_id)]
)
ToolNode([list_update_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "list_update_tool"}
],
)
]
}
)
# test validation (missing tool message in the update for current graph)
with pytest.raises(ValueError):
@dec_tool
def no_update_tool():
"""My tool"""
return Command(update={"messages": []})
ToolNode([no_update_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[{"args": {}, "id": "1", "name": "no_update_tool"}],
)
]
}
)
# test validation (tool message with a wrong tool call ID)
with pytest.raises(ValueError):
@dec_tool
def mismatching_tool_call_id_tool():
"""My tool"""
return Command(
update={"messages": [ToolMessage(content="foo", tool_call_id="2")]}
)
ToolNode([mismatching_tool_call_id_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{
"args": {},
"id": "1",
"name": "mismatching_tool_call_id_tool",
}
],
)
]
}
)
# test validation (missing tool message in the update for parent graph is OK)
@dec_tool
def node_update_parent_tool():
"""No update"""
return Command(update={"messages": []}, graph=Command.PARENT)
assert ToolNode([node_update_parent_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "node_update_parent_tool"}
],
)
]
}
) == [Command(update={"messages": []}, graph=Command.PARENT)]
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,
reason="Langchain core 0.3.0 or greater is required",
)
async def test_tool_node_command_list_input():
from langchain_core.tools.base import InjectedToolCallId
@dec_tool
def transfer_to_bob(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Transfer to Bob"""
return Command(
update=[
ToolMessage(content="Transferred to Bob", tool_call_id=tool_call_id)
],
goto="bob",
graph=Command.PARENT,
)
@dec_tool
async def async_transfer_to_bob(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Transfer to Bob"""
return Command(
update=[
ToolMessage(content="Transferred to Bob", tool_call_id=tool_call_id)
],
goto="bob",
graph=Command.PARENT,
)
class CustomToolSchema(BaseModel):
tool_call_id: Annotated[str, InjectedToolCallId]
class MyCustomTool(BaseTool):
def _run(*args: Any, **kwargs: Any):
return Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id=kwargs["tool_call_id"],
)
],
goto="bob",
graph=Command.PARENT,
)
async def _arun(*args: Any, **kwargs: Any):
return Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id=kwargs["tool_call_id"],
)
],
goto="bob",
graph=Command.PARENT,
)
custom_tool = MyCustomTool(
name="custom_transfer_to_bob",
description="Transfer to bob",
args_schema=CustomToolSchema,
)
async_custom_tool = MyCustomTool(
name="async_custom_transfer_to_bob",
description="Transfer to bob",
args_schema=CustomToolSchema,
)
# test mixing regular tools and tools returning commands
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
result = ToolNode([add, transfer_to_bob]).invoke(
[
AIMessage(
"",
tool_calls=[
{"args": {"a": 1, "b": 2}, "id": "1", "name": "add"},
{"args": {}, "id": "2", "name": "transfer_to_bob"},
],
)
]
)
assert result == [
[
ToolMessage(
content="3",
tool_call_id="1",
name="add",
)
],
Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id="2",
name="transfer_to_bob",
)
],
goto="bob",
graph=Command.PARENT,
),
]
# test tools returning commands
# test sync tools
for tool in [transfer_to_bob, custom_tool]:
result = ToolNode([tool]).invoke(
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])]
)
assert result == [
Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name=tool.name,
)
],
goto="bob",
graph=Command.PARENT,
)
]
# test async tools
for tool in [async_transfer_to_bob, async_custom_tool]:
result = await ToolNode([tool]).ainvoke(
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])]
)
assert result == [
Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name=tool.name,
)
],
goto="bob",
graph=Command.PARENT,
)
]
# test multiple commands
result = ToolNode([transfer_to_bob, custom_tool]).invoke(
[
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "transfer_to_bob"},
{"args": {}, "id": "2", "name": "custom_transfer_to_bob"},
],
)
]
)
assert result == [
Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id="1",
name="transfer_to_bob",
)
],
goto="bob",
graph=Command.PARENT,
),
Command(
update=[
ToolMessage(
content="Transferred to Bob",
tool_call_id="2",
name="custom_transfer_to_bob",
)
],
goto="bob",
graph=Command.PARENT,
),
]
# test validation (mismatch between input type and command.update type)
with pytest.raises(ValueError):
@dec_tool
def list_update_tool(tool_call_id: Annotated[str, InjectedToolCallId]):
"""My tool"""
return Command(
update={
"messages": [ToolMessage(content="foo", tool_call_id=tool_call_id)]
}
)
ToolNode([list_update_tool]).invoke(
[
AIMessage(
"",
tool_calls=[{"args": {}, "id": "1", "name": "list_update_tool"}],
)
]
)
# test validation (missing tool message in the update for current graph)
with pytest.raises(ValueError):
@dec_tool
def no_update_tool():
"""My tool"""
return Command(update=[])
ToolNode([no_update_tool]).invoke(
[
AIMessage(
"",
tool_calls=[{"args": {}, "id": "1", "name": "no_update_tool"}],
)
]
)
# test validation (tool message with a wrong tool call ID)
with pytest.raises(ValueError):
@dec_tool
def mismatching_tool_call_id_tool():
"""My tool"""
return Command(update=[ToolMessage(content="foo", tool_call_id="2")])
ToolNode([mismatching_tool_call_id_tool]).invoke(
[
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "mismatching_tool_call_id_tool"}
],
)
]
)
# test validation (missing tool message in the update for parent graph is OK)
@dec_tool
def node_update_parent_tool():
"""No update"""
return Command(update=[], graph=Command.PARENT)
assert ToolNode([node_update_parent_tool]).invoke(
[
AIMessage(
"",
tool_calls=[{"args": {}, "id": "1", "name": "node_update_parent_tool"}],
)
]
) == [Command(update=[], graph=Command.PARENT)]
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,
reason="Langchain core 0.3.0 or greater is required",
)
def test_react_agent_update_state():
from langchain_core.tools.base import InjectedToolCallId
class State(AgentState):
user_name: str
@dec_tool
def get_user_name(tool_call_id: Annotated[str, InjectedToolCallId]):
"""Retrieve user name"""
user_name = interrupt("Please provider user name:")
return Command(
update={
"user_name": user_name,
"messages": [
ToolMessage(
"Successfully retrieved user name", tool_call_id=tool_call_id
)
],
}
)
def state_modifier(state: State):
user_name = state.get("user_name")
if user_name is None:
return state["messages"]
system_msg = f"User name is {user_name}"
return [{"role": "system", "content": system_msg}] + state["messages"]
checkpointer = MemorySaver()
tool_calls = [[{"args": {}, "id": "1", "name": "get_user_name"}]]
model = FakeToolCallingModel(tool_calls=tool_calls)
agent = create_react_agent(
model,
[get_user_name],
state_schema=State,
state_modifier=state_modifier,
checkpointer=checkpointer,
)
config = {"configurable": {"thread_id": "1"}}
# run until interrpupted
agent.invoke({"messages": [("user", "what's my name")]}, config)
# supply the value for the interrupt
response = agent.invoke(Command(resume="Archibald"), config)
# confirm that the state was updated
assert response["user_name"] == "Archibald"
assert len(response["messages"]) == 4
tool_message: ToolMessage = response["messages"][-2]
assert tool_message.content == "Successfully retrieved user name"
assert tool_message.tool_call_id == "1"
assert tool_message.name == "get_user_name"
def my_function(some_val: int, some_other_val: str) -> str:
return f"{some_val} - {some_other_val}"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+59 -1
View File
@@ -21,7 +21,11 @@ from typing_extensions import Annotated, NotRequired, Required
from langgraph.graph import END, StateGraph
from langgraph.graph.graph import CompiledGraph
from langgraph.utils.fields import _is_optional_type, get_field_default
from langgraph.utils.fields import (
_is_optional_type,
get_enhanced_type_hints,
get_field_default,
)
from langgraph.utils.runnable import is_async_callable, is_async_generator
pytestmark = pytest.mark.anyio
@@ -227,3 +231,57 @@ def test_is_required():
assert get_field_default("val_12", gcannos["val_12"], MyGrandChildDict) is None
assert get_field_default("val_9", gcannos["val_9"], MyGrandChildDict) is None
assert get_field_default("val_13", gcannos["val_13"], MyGrandChildDict) == ...
def test_enhanced_type_hints() -> None:
from dataclasses import dataclass
from typing import Annotated
from pydantic import BaseModel, Field
class MyTypedDict(TypedDict):
val_1: str
val_2: int = 42
val_3: str = "default"
hints = list(get_enhanced_type_hints(MyTypedDict))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", None)
@dataclass
class MyDataclass:
val_1: str
val_2: int = 42
val_3: str = "default"
hints = list(get_enhanced_type_hints(MyDataclass))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", None)
class MyPydanticModel(BaseModel):
val_1: str
val_2: int = 42
val_3: str = Field(default="default", description="A description")
hints = list(get_enhanced_type_hints(MyPydanticModel))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, None)
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", "A description")
class MyPydanticModelWithAnnotated(BaseModel):
val_1: Annotated[str, Field(description="A description")]
val_2: Annotated[int, Field(default=42)]
val_3: Annotated[
str, Field(default="default", description="Another description")
]
hints = list(get_enhanced_type_hints(MyPydanticModelWithAnnotated))
assert len(hints) == 3
assert hints[0] == ("val_1", str, None, "A description")
assert hints[1] == ("val_2", int, 42, None)
assert hints[2] == ("val_3", str, "default", "Another description")
@@ -191,6 +191,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
@@ -257,6 +258,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
@@ -353,6 +355,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
@@ -459,6 +462,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
@@ -520,6 +524,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
@@ -637,6 +642,7 @@ async def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
@@ -190,6 +190,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
@@ -256,6 +257,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_store": None,
"__pregel_dedupe_tasks": True,
@@ -352,6 +354,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_store": None,
@@ -457,6 +460,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_store": None,
@@ -518,6 +522,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_store": None,
@@ -635,6 +640,7 @@ def test_subgraph_w_interrupt(
"__pregel_delegate": False,
"__pregel_read": None,
"__pregel_send": None,
"__pregel_call": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.31",
"version": "0.0.32",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",

Some files were not shown because too many files have changed in this diff Show More