Compare commits

..
Author SHA1 Message Date
Vadym BardaandGitHub 22e6468af5 langgraph: release 0.2.66 (#3128) 2025-01-21 13:26:29 -05:00
ccurmeandGitHub 24bc0c0630 docs: update readme / docs intro page (#3082) 2025-01-21 13:19:30 -05:00
Nuno CamposandGitHub 802e6df8df Enable async tests that were being skipped (#3109)
- async tests are placed in test_pregel_async, not in test_pregel
- to avoid tests placed in wrong file being accidentally skipped i've
added the auto-async mark to sync test file
2025-01-21 10:16:20 -08:00
Nuno CamposandGitHub 3ec55b008d Fix timing issue where a sync task would finish before the other one was registered in futures dict (#3110)
- this was not possible in async where all done callbacks are called in
next tick
- in sync case this would manifest as the first task done callback
seeing counter == 1 and thus setting event
- the fix is to unset the event whenever a task is scheduled
2025-01-21 10:16:09 -08:00
Nuno CamposandGitHub e10b7c1391 Re-enable support for running sync tasks from async entrypoints (#3108)
- When using an async entrypoint you can now freely mix and match sync
and async tasks with a uniform api (ie all tasks return a sync or async
future depending on context)
- Fix issues with scheduling deeply nested tasks (use threadsafe methods
to schedule coroutines and create futures)
2025-01-21 10:15:49 -08:00
Nuno CamposandGitHub 31cc6b9f1d Fix tracing of args for @task decorated functions (#3107)
- now using same logic as in langsmith sdk, treating as single args
dict, based on function signature
2025-01-21 10:09:13 -08:00
Nuno Campos 2cafb4905b Lint 2025-01-21 10:06:02 -08:00
Nuno Campos fe46576d98 Fix 2025-01-21 09:59:49 -08:00
Nuno Campos 16c86c9de6 Add test 2025-01-21 09:49:46 -08:00
fca0d2d5bb Update libs/langgraph/langgraph/utils/future.py
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-01-21 09:43:12 -08:00
Nuno Campos c7c62f5587 Fix 2025-01-21 09:42:25 -08:00
Vadym BardaandGitHub 956c5f68fc docs: add a how-to guide for structured outputs in react agent (#3121) 2025-01-21 17:30:32 +00:00
ccurmeandGitHub 4908caf522 docs: how-to guides nits (#3123) 2025-01-21 12:17:37 -05:00
Chester Curme f926eada24 fix warnings 2025-01-21 12:06:42 -05:00
Chester Curme adc1e47028 nits 2025-01-21 12:00:12 -05:00
Vadym BardaandGitHub 7820b5c765 docs: add docs for Command.PARENT (#3081) 2025-01-21 16:37:39 +00:00
Chester Curme ca6f8e2042 Merge branch 'main' into cc/update_readme 2025-01-21 10:31:29 -05:00
Chester Curme ee61d06f8d add hyperlink 2025-01-21 10:31:27 -05:00
Roy BarberandGitHub 6eeb9de46a DOCS: Incorrect reference to JS/TS SDK as Python SDK (#3103)
Small typo in the Local Server page:
https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/
2025-01-21 09:34:57 -05:00
viren-viiandGitHub 19a91f4677 Update persistence_postgres.ipynb to remove typo in markdown cell. (#3119)
Fixed a typo that was interrupting the markdown.
2025-01-21 09:33:31 -05:00
BagaturandGitHub 6087b1969e python[patch]: call create_react_agent model node without is_last_step (#3114)
So that you can call agent.nodes['agent'].invoke({'messages': []})
without needing to specify is_last_step. very helpful for evaluating
just the model node of the agent
2025-01-20 18:28:57 -08:00
Nuno CamposandGitHub 6cbc7e8b67 Add get_store function (#3112) 2025-01-20 16:12:39 -08:00
Nuno Campos b2213e523e Add get_store function 2025-01-20 16:03:27 -08:00
David DuongandGitHub ac64f50383 feat(cli): add detection for bun.lockb (#3111) 2025-01-21 00:30:22 +01:00
Tat Dat Duong 25239891bc feat(cli): add detection for bun.lockb 2025-01-21 00:17:12 +01:00
Nuno Campos b7c3ac4501 Fix tracing output 2025-01-20 13:55:13 -08:00
Nuno Campos 09e8516689 Fix test 2025-01-20 11:57:19 -08:00
Nuno Campos 3ca75d69b8 Lint 2025-01-20 11:57:19 -08:00
Nuno Campos d48dec5452 Enable async tests that were being skipped
- async tests are placed in test_pregel_async, not in test_pregel
- to avoid tests placed in wrong file being accidentally skipped i've added the auto-async mark to sync test file
2025-01-20 11:57:19 -08:00
Nuno Campos d48b25420b Fix timing issue where a sync task would finish before the other one was registered in futures dict
- this was not possible in async where all done callbacks are called in next tick
- in sync case this would manifest as the first task done callback seeing counter == 1 and thus setting event
- the fix is to unset the event whenever a task is scheduled
2025-01-20 11:40:54 -08:00
Nuno Campos 12be3fac33 Lint 2025-01-20 11:13:37 -08:00
Nuno Campos aed1f0ba18 Re-enable support for running sync tasks from async entrypoints
- When using an async entrypoint you can now freely mix and match sync and async tasks with a uniform api (ie all tasks return a sync or async future depending on context)
- Fix issues with scheduling deeply nested tasks (use threadsafe methods to schedule coroutines and create futures)
2025-01-20 11:03:51 -08:00
Nuno Campos 07695f5c5a Lint 2025-01-20 10:53:37 -08:00
Nuno Campos 204c9c83f8 Fix tracing of args for @task decorated functions
- now using same logic as in langsmith sdk, treating as single args dict, based on function signature
2025-01-20 10:49:27 -08:00
David DuongandGitHub 0bdd27ade4 fix(cli): warn users to use the JS cli for JS graphs (#3086) 2025-01-18 02:29:45 +01:00
David DuongandGitHub ab0048981a docs: add mention of JS CLI (#3077) 2025-01-18 01:52:00 +01:00
Tat Dat DuongandNuno Campos e18f2b3795 fix(cli): warn users to use the JS cli for JS graphs 2025-01-17 16:07:31 -08:00
Chester CurmeandNuno Campos 31578cbe0e move code block 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 44970640d5 cr 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 0f4e42474f cr 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 832f9ad64e copy changes to libs/langgraph/README.md 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 318de5bb81 update readme 2025-01-17 16:07:16 -08:00
William FHandGitHub a11f9b3535 Fix docstring in pg store init (#3094) 2025-01-18 00:03:51 +00:00
Nuno CamposandGitHub 29db5a8672 Implement get_graph for imperative api (#3076) 2025-01-17 15:53:50 -08:00
Nuno CamposandGitHub 444faec6e6 Fix two issues with task/stream timing (#3095)
- both issues are related to the fact that waiters for futures are
notified of completion before "done" callbacks are called
- 1st issue manifested as interrupt stream event being emitted before
the result of a task that logically finished first (it's in the line
above in body of the entrypoint function) -> this is solved by always
returning to use code a fresh future chained on the original future,
because chaining is done via done callbacks (therefore the chained
future will only resolve after done callbacks of the original feature
are called)
- 2nd issue mainfested as sometimes (very rarely) the last stream event
not being printed before stream() finishes. this is solved by ensuring
we only return out of PregelRunner.tick() once all "done" callbacks are
called, previously we were approximating this through use of
asyncio.sleep(0) / time.sleep(0). The new solution instead waits on a
threading/asyncio.Event which will only be set by the last "done"
callback to fire
- this PR also disables incomplete support for calling sync tasks from
async entrypoints
2025-01-17 15:44:52 -08:00
Nuno Campos e4a5c8fd28 Fix two issues with task/stream timing
- both issues are related to the fact that waiters for futures are notified of completion before "done" callbacks are called
- 1st issue manifested as interrupt stream event being emitted before the result of a task that logically finished first (it's in the line above in body of the entrypoint function) -> this is solved by always returning to use code a fresh future chained on the original future, because chaining is done via done callbacks (therefore the chained future will only resolve after done callbacks of the original feature are called)
- 2nd issue mainfested as sometimes (very rarely) the last stream event not being printed before stream() finishes. this is solved by ensuring we only return out of PregelRunner.tick() once all "done" callbacks are called, previously we were approximating this through use of asyncio.sleep(0) / time.sleep(0). The new solution instead waits on a threading/asyncio.Event which will only be set by the last "done" callback to fire
2025-01-17 15:35:08 -08:00
Tat Dat Duong 0f1b0bfba3 typo 2025-01-17 23:40:58 +01:00
Tat Dat Duong 0e79407973 Last pass 2025-01-17 23:40:58 +01:00
Tat Dat Duong efa51fe22c Update README.md 2025-01-17 23:40:58 +01:00
Tat Dat Duong 11f2501c98 docs: add mention of JS CLI 2025-01-17 23:40:58 +01:00
Eugene YurtsevandGitHub 71e6002a3c docs: update broken api reference (#3093) 2025-01-17 17:04:54 -05:00
Andrew NguonlyandGitHub 26e3ded701 docs: Add note about cron jobs not available in LangGraph Platform Self-Hosted Lite (#3091) 2025-01-17 12:52:46 -08:00
Eugene YurtsevandGitHub 4fcd1690f9 docs: expose functional api reference (#3089)
The API reference is marked as experimental right now, so this is OK to expose to make it easier to share w/ beta users.
2025-01-17 15:18:43 -05:00
Vadym BardaandGitHub eb33a873c8 langgraph: release 0.2.64 (#3090) 2025-01-17 15:18:34 -05:00
91aa66f4cf tests: add tests for calling multiple subgraphs in a parent node (#3070)
Co-authored-by: Nuno Campos <nuno@langchain.dev>
2025-01-17 15:16:01 -05:00
Eugene Yurtsev 4476640702 x 2025-01-17 15:08:32 -05:00
Eugene YurtsevandGitHub 727e3f1730 functional api: first pass at api reference (#3083)
First pass at the API reference for the functional API
2025-01-17 14:46:22 -05:00
Vadym BardaandGitHub 943dd28863 docs: update state to include 'next' for supervisor notebooks (#3087) 2025-01-17 14:32:15 -05:00
ccurmeandGitHub aa9d253978 docs: highlight lines in troubleshooting doc (#3084)
![Screenshot 2025-01-17 at 1 44
07 PM](https://github.com/user-attachments/assets/b7009c77-cb2e-43e0-92a7-3978884b7f3e)
2025-01-17 18:53:21 +00:00
Eugene YurtsevandGitHub a11c6cfe6b Update docs/mkdocs.yml 2025-01-17 13:36:44 -05:00
Eugene Yurtsev 15c79dc3c3 x 2025-01-17 13:34:49 -05:00
Eugene Yurtsev a65c9f1e04 x 2025-01-17 13:11:23 -05:00
9912ae1053 docs: add high level example into the readme, make examples collapsible (#3052)
Co-authored-by: Chester Curme <chester.curme@gmail.com>
2025-01-17 17:10:22 +00:00
ccurmeandGitHub 7db29042b4 docs[patch]: update quickstart tutorial to use interrupt() (#3053)
Need to update cassettes
2025-01-17 11:24:40 -05:00
Chester Curme f8033a20c1 update cassettes 2025-01-17 11:15:33 -05:00
Chester Curme 5e3aa495d9 update 2025-01-17 11:05:55 -05:00
Chester Curme a54689affd ruff 2025-01-17 11:02:58 -05:00
Chester Curme 02ea523897 udpate 2025-01-17 11:02:25 -05:00
Chester Curme 18c6637e6c use message dicts instead of tuples 2025-01-17 10:34:21 -05:00
Chester Curme 4c02a4c501 Revert "disable parallel tool use"
This reverts commit 784b6afa11.
2025-01-17 10:19:17 -05:00
Chester Curme 784b6afa11 disable parallel tool use 2025-01-17 10:15:06 -05:00
Chester Curme b35b892d74 Merge branch 'main' into cc/update_quickstart 2025-01-17 10:02:29 -05:00
Chester Curme e7aaf21121 update 2025-01-17 10:02:18 -05:00
Eugene Yurtsev 54115ac796 x 2025-01-17 09:58:06 -05:00
ccurmeandGitHub 76be64adcb docs[patch]: fix builds (#3074) 2025-01-17 02:41:47 +00:00
Nuno Campos 2cf98725c4 Lint 2025-01-16 16:32:39 -08:00
Nuno Campos a33c59626a Lint 2025-01-16 16:01:57 -08:00
Nuno Campos 56d3b759c7 Add one more test 2025-01-16 15:46:25 -08:00
Nuno Campos 32bf81c559 Lint 2025-01-16 14:10:16 -08:00
Eugene Yurtsev de332ec31c x 2025-01-16 17:08:12 -05:00
Eugene YurtsevandGitHub ce30965df4 functional api: Add ability to request previous output (#3025)
1. The inputs into foo do not affect any state behavior
2. `previous` always reflects the previous return value from the
function
3. Anything can be returned and that will be the new state for the
function on the next iteration
4. This API is not meant to support reducers in the inputs/state

```python
  from langgraph.func import entrypoint

  states = []

  # In this version reducers do not work
  @entrypoint(checkpointer=MemorySaver())
  def foo(inputs, *, previous: Any) -> Any:
      states.append(previous)
      return {"previous": previous, "current": inputs}

  config = {"configurable": {"thread_id": "1"}}

  foo.invoke({"a": "1"}, config)
  foo.invoke({"a": "2"}, config)
  foo.invoke({"a": "3"}, config)
  assert states == [
      None,
      {"current": {"a": "1"}, "previous": None},
      {"current": {"a": "2"}, "previous": {"current": {"a": "1"}, "previous": None}},
  ]
```
2025-01-16 17:02:41 -05:00
Nuno Campos b98a7b09a3 Update 2025-01-16 13:44:19 -08:00
Nuno Campos 86b6afc982 Implement get_graph for imperative api 2025-01-16 13:44:18 -08:00
Nuno CamposandGitHub 47122ce88b docs: Add status field to Project object for Control Plane API (#3075) 2025-01-16 13:42:48 -08:00
Andrew Nguonly 002b048674 Add status field to Project object. 2025-01-16 13:27:23 -08:00
Eugene Yurtsev a58f5dacca x 2025-01-16 16:26:54 -05:00
Chester Curme cc397b1629 update cassettes 2025-01-16 16:24:05 -05:00
Eugene Yurtsev 3a48049194 x 2025-01-16 16:19:23 -05:00
Eugene Yurtsev e5f0db0af3 x 2025-01-16 16:09:11 -05:00
Chester Curme 93854ed721 fix link 2025-01-16 15:12:53 -05:00
Eugene Yurtsev 83a7acc57c Merge branch 'main' into eugene/expose_previous_state 2025-01-16 14:56:46 -05:00
Eugene Yurtsev b218cc76a7 type ignore for now 2025-01-16 14:50:40 -05:00
Eugene Yurtsev d021f476db x 2025-01-16 14:23:32 -05:00
Chester Curme d978cb0392 part 7 -> part 6 2025-01-16 14:21:40 -05:00
Chester Curme 605fe4ea11 simplify hitl, condense part 5 2025-01-16 14:20:05 -05:00
Eugene Yurtsev 46907b6cf9 x 2025-01-16 14:04:00 -05:00
Eugene Yurtsev 582856b30c x 2025-01-16 14:00:42 -05:00
Eugene Yurtsev e476897177 fix merge error 2025-01-16 13:58:55 -05:00
Eugene Yurtsev 421f7c0238 x 2025-01-16 13:55:41 -05:00
Eugene Yurtsev 3095bfce9c Merge branch 'main' into eugene/expose_previous_state 2025-01-16 13:54:05 -05:00
Nuno CamposandGitHub 4cfe0b2d4c tests: add a test for interrupt() w/ functional API (#3065) 2025-01-16 10:04:17 -08:00
Nuno Campos a16def5140 Lint 2025-01-16 09:55:04 -08:00
Nuno CamposandGitHub 1e730d124c Make config schema configurable for imperative api (#3067) 2025-01-16 09:52:55 -08:00
ccurmeandGitHub 21a7105655 docs[patch]: enable navigation back to index pages from child pages via sidebar (#3069)
Currently, if you're viewing a how-to guide and you click "How-to
Guides" in the sidebar, you aren't navigated back to the index page (it
will work if you click on a different guides section). To get back to
the index page, you need to scroll up and click the breadcrumbs.

After this change, clicking the link in the sidebar should navigate you
to the index page regardless of the page you are viewing.

Only side-effect from what I can tell is that "Home > Introduction" just
becomes "**Home**", which I think is fine (maybe preferable).

Before:
![Screenshot 2025-01-16 at 11 16
26 AM](https://github.com/user-attachments/assets/e4bdedfc-ed52-4a66-b0b2-194383ce2043)

After:
![Screenshot 2025-01-16 at 11 16
00 AM](https://github.com/user-attachments/assets/4d1d935b-5b11-43ac-8b63-c5aaff871188)
2025-01-16 12:52:16 -05:00
Nuno Campos 8c88e203bc Fix 2025-01-16 09:51:46 -08:00
Nuno CamposandGitHub 2a1224966c Implement input/output schemas for imperative api (#3066) 2025-01-16 08:45:02 -08:00
vbarda c4460e5dd2 add another test 2025-01-16 11:20:09 -05:00
Nuno Campos 46056363b3 Make config schema configurable for imperative api 2025-01-16 08:00:08 -08:00
Nuno Campos ba672604a6 Implement input/output schemas for imperative api 2025-01-16 07:57:00 -08:00
vbarda 8b1597a385 tests: add a test for interrupt() w/ functional API 2025-01-16 10:16:26 -05:00
Vadym BardaandGitHub adff439d4e langgraph: release 0.2.63 (#3064) 2025-01-16 09:19:25 -05:00
Vadym BardaandGitHub 89b0be3a7d checkpoint-postgres: release 2.0.13 (#3063) 2025-01-16 09:17:43 -05:00
Vadym BardaandGitHub 7ecda42b42 checkpoint-postgres: bring back missing migration (#3058) 2025-01-16 03:16:28 +00:00
Nuno CamposandGitHub bd99471705 tests: Add test for catching bunching in streaming (#3057) 2025-01-15 18:57:07 -08:00
Nuno Campos 0e4dbb4c62 Guard 2025-01-15 18:36:49 -08:00
Nuno Campos 145220f2a8 Fix 2025-01-15 18:29:34 -08:00
Eugene Yurtsev f9c25bba07 x 2025-01-15 21:15:38 -05:00
Eugene Yurtsev 7ecad39ecb x 2025-01-15 21:15:05 -05:00
Nuno CamposandGitHub 6bfc6be307 Add support for multiple subgraphs called in a single node (#3056) 2025-01-15 18:01:31 -08:00
Nuno Campos 6b9369876f Fix sync 2025-01-15 17:52:19 -08:00
Nuno Campos c26b0e78b6 Fix kafka lib 2025-01-15 17:15:58 -08:00
Nuno CamposandGitHub 1763dd69a7 Add checkpointer=True mode for subgraphs that want to keep state between turns (#3055) 2025-01-15 16:08:03 -08:00
Nuno Campos f4bd023ab1 Fix 2025-01-15 16:04:14 -08:00
Nuno Campos d402bf7379 Remove flag 2025-01-15 15:36:16 -08:00
Nuno Campos e8a73e1505 Remove flag 2025-01-15 15:36:10 -08:00
Nuno Campos d6492ef048 Add support for multiple subgraphs called in a single node 2025-01-15 15:36:10 -08:00
Nuno Campos 38d9b39f6e Add flag 2025-01-15 15:36:03 -08:00
Nuno Campos be8b4a1d7f Lint 2025-01-15 15:30:29 -08:00
Nuno Campos 71fbd6a8b4 Lint 2025-01-15 15:29:34 -08:00
Nuno Campos 5375af7827 Add checkpointer=True mode for subgraphs that want to keep state betweenn turns 2025-01-15 15:15:44 -08:00
Chester Curme 1654062957 enable navigation.indexes 2025-01-15 18:14:37 -05:00
Eugene Yurtsev 6b707cbfc5 x 2025-01-15 17:15:56 -05:00
Nuno CamposandGitHub dd7ac00953 Fix unexpected re-use of null resume value by subgraphs (#3054)
- Also stop exposing writes in config, in favor of scratchpad
2025-01-15 13:59:13 -08:00
Nuno Campos cd64075928 Lint 2025-01-15 13:49:33 -08:00
Nuno Campos 144ee31546 Lint 2025-01-15 13:43:14 -08:00
Nuno CamposandGitHub 8bb84c7096 Fix ignored goto when a mixed list of command and state updates is returned from a node (#3038) 2025-01-15 13:39:59 -08:00
Nuno Campos e83660885b Lint 2025-01-15 13:39:44 -08:00
Nuno Campos c2a57385c0 Update tests 2025-01-15 13:35:51 -08:00
Nuno Campos 3626478029 Fix unexpected re-use of null resume value by subgraphs
- Also stop exposing writes in config, in favor of scratchpad
2025-01-15 13:31:49 -08:00
Chester Curme 30311f3fb9 ruff 2025-01-15 16:12:01 -05:00
Chester Curme 0f458fbaff update 2025-01-15 16:06:55 -05:00
Brace SproulandGitHub 13c9bfa282 feat(langgraph): Add interrupt schema to library (#2947) 2025-01-15 12:55:47 -08:00
Andrew NguonlyandGitHub b6fe3937fc docs: Add section about Persistence to Cloud SaaS concepts page (#3051) 2025-01-15 12:27:53 -08:00
Chester Curme d7bae594f0 update hitl 2025-01-15 15:04:43 -05:00
Nuno Campos 5805e5709a Fix ignored goto when a mixed list of command and state updates is returned from a node 2025-01-15 11:42:28 -08:00
Nuno CamposandGitHub aab6fdf3f3 Fix Send order after interrupt/resume (#3037)
- order was incorrectly based on task id, instead of the correct task
path
- this requires storing task paths on checkpointers
- addition of task_path to put_writes is made backwards compatible by
checking signature on call, and treating it as an optional arg
2025-01-15 11:42:08 -08:00
Nuno CamposandGitHub be1d035aba tests: add test for multiple interrupts and tasks (#2941) 2025-01-15 11:41:30 -08:00
Nuno Campos 6e228f8a9c Lint 2025-01-15 11:32:42 -08:00
Nuno Campos 0adbd89d9a Bump checkpoint 2025-01-15 11:28:55 -08:00
Nuno Campos 47c37d140f Fix Send order after interrupt/resume
- order was incorrectly based on task id, instead of the correct task path
- this requires storing task paths on checkpointers
- addition of task_path to put_writes is made backwards compatible by checking signature on call, and treating it as an optinal arg
2025-01-15 11:28:26 -08:00
Nuno Campos fc887f7a5d checkpoint-postgres/sqlite 2.0.12/2.0.3 2025-01-15 11:26:51 -08:00
ccurmeandGitHub bbfc2b7370 docs[patch]: add section headers to LangGraph concepts page (#3047)
![Screenshot 2025-01-15 at 1 03
45 PM](https://github.com/user-attachments/assets/c7a6d5b7-cc42-4616-bba0-4352c2f922bc)
2025-01-15 14:15:33 -05:00
Nuno Campos 2c984dfe4e checkpoint 2.0.10 2025-01-15 11:14:03 -08:00
Nuno CamposandGitHub 09c0d9cec3 Add optional task_path arg for put_writes() (#3049)
- Will be used for sorting pending_sends when available
2025-01-15 11:11:10 -08:00
Nuno Campos 31734fb792 Lint 2025-01-15 11:02:10 -08:00
Nuno Campos eed577ee2a Update sqlite signature 2025-01-15 10:58:46 -08:00
Nuno Campos bba00506ea Lint 2025-01-15 10:57:26 -08:00
Nuno Campos fa12538a4e Lint 2025-01-15 10:50:11 -08:00
Nuno Campos c1a7bb8902 Lint 2025-01-15 10:49:47 -08:00
Nuno Campos 14b500d8a8 Fix 2025-01-15 10:47:09 -08:00
Nuno Campos 053a501db3 Add optional task_path arg for put_writes()
- Will be used for sorting pending_sends when available
2025-01-15 10:42:54 -08:00
Andrew NguonlyandGitHub bbe8eacf8d docs: Update Studio FAQ (#3048)
### Screenshot

![image](https://github.com/user-attachments/assets/6da2faa6-1c62-44e4-96f8-3450f94cb15c)
2025-01-15 10:23:43 -08:00
Nuno Campos 931d39124c Fix 2025-01-15 10:23:30 -08:00
Nuno Campos a7bb96da98 Fix 2025-01-15 10:21:40 -08:00
Nuno Campos 1e9a372dd7 Fix multiple interrupt/task test 2025-01-15 10:11:31 -08:00
Chester Curme 5030f1e89c bold -> headers 2025-01-15 13:02:59 -05:00
Nuno CamposandGitHub 50cb387904 Remove duckdb checkpointer and store (#3046)
- duckdb is too buggy to be able to provide reliable checkpointer and
store
2025-01-15 09:23:49 -08:00
Nuno Campos 003b1883c9 Remove duckdb checkpointer and store
- duckdb is too buggy to be able to provide reliable checkpointer and store
2025-01-15 09:15:17 -08:00
Nuno Campos 17aebb6239 Fix flasy return from task 2025-01-15 09:14:36 -08:00
Nuno CamposandGitHub d12830e95c Fix tracing hierarchy for imperative api (#3036) 2025-01-15 09:10:45 -08:00
Eugene YurtsevandNuno Campos 29b70cbf39 x 2025-01-15 09:03:46 -08:00
Eugene YurtsevandNuno Campos 6b86fbb0a8 x 2025-01-15 09:03:46 -08:00
Nuno CamposandGitHub 4426552c26 Fix flaky test output order (#3045) 2025-01-15 09:02:05 -08:00
Nuno Campos 671f268651 Remove duckdb checkpointer and store
- duckdb is too buggy to be able to provide reliable checkpointer and store
2025-01-15 08:56:32 -08:00
Vadym BardaandGitHub e2554c9616 langgraph: fix non-empty value check in ensure_config (#3039)
Fixes https://github.com/langchain-ai/langgraph/issues/2890
2025-01-15 16:54:31 +00:00
Nuno CamposandGitHub f1ee650489 Remove CI job to test against core 0.2.x (#3044) 2025-01-15 08:52:58 -08:00
Nuno Campos b7e4656c91 Fix flaky test output order 2025-01-15 08:45:35 -08:00
Nuno Campos 65a41942ef Remove CI job to test against core 0.2.x 2025-01-15 08:39:09 -08:00
Nuno Campos c767d86c9c Remove ci job for removed flag 2025-01-15 08:37:24 -08:00
Nuno Campos 01331ef858 Lint 2025-01-15 08:33:09 -08:00
Nuno CamposandGitHub 24a4c67c52 Merge branch 'main' into nc/14jan/fix-tracing-hierarchy-imperative 2025-01-15 08:31:38 -08:00
Nuno CamposandGitHub c7e43f86d4 Remove send v2 (#3033) 2025-01-15 08:29:05 -08:00
Vadym BardaandGitHub b18d266f2c langgraph: allow model names as strings in create_react_agent (#3031)
```python
agent = create_react_agent("anthropic:claude-3-5-sonnet-latest", [add])
agent.invoke({"messages": [("user", "what's 3 + 5")]})
```
2025-01-15 09:43:37 -05:00
Eugene YurtsevandGitHub 536ee7b37a docs: document inject kwarg (#3026) 2025-01-14 21:41:27 -05:00
Nuno Campos 901273c0e2 Lint 2025-01-14 17:09:30 -08:00
Nuno Campos 7bdbd62611 Fix tracing hierarchy for imperative api 2025-01-14 17:03:45 -08:00
Nuno Campos 0b74e25f72 Remove send v2 2025-01-14 15:49:57 -08:00
Eugene YurtsevandGitHub ff843ab005 docs: internal doc nits (#2946)
Update internal documentation
2025-01-14 23:16:38 +00:00
Eugene Yurtsev 7603809a9f x 2025-01-14 18:06:09 -05:00
Vadym BardaandGitHub f7fae7e140 langgraph: fix ismethod check in add_node (#3032)
Fixes #2893 #2965
2025-01-14 17:17:50 -05:00
Eugene YurtsevandGitHub 651ee8bd24 Update local-server.md (#3030) 2025-01-14 16:22:26 -05:00
Eugene YurtsevandGitHub 54bdba2da9 Update introduction.ipynb (#3029) 2025-01-14 16:20:54 -05:00
Eugene YurtsevandGitHub 7e5806cb0b Update local-server.md 2025-01-14 16:13:57 -05:00
Eugene YurtsevandGitHub 66f674fead Update introduction.ipynb 2025-01-14 16:12:20 -05:00
Vadym BardaandGitHub f029d615e6 ci: fix docs build (#3028) 2025-01-14 15:58:03 -05:00
Eugene Yurtsev bd76773b31 x 2025-01-14 15:50:20 -05:00
Eugene Yurtsev 311fe3970c x 2025-01-14 15:39:57 -05:00
Eugene Yurtsev 67a800dd98 update 2025-01-14 15:37:10 -05:00
ccurmeandGitHub 9b6e6d67dc layout (#2972)
Some docs layout improvements to help guide user journey.

Currently we have `Home | Tutorials | How-tos | Concepts | Reference` in
top-level horizontal navigation bar.

Here we make these updates:
- Top-level horizontal navigation bar is just `Home | API Reference`
- Add vertical sidebar to `Home` with sections:
  - Introduction
  - Get started
  - Guides
  - Resources

`Get Started` contains quickstarts for LG and LG Platform / deployment.
These are tutorials in Diataxis terms.

`Guides` contains index pages for how-tos, concepts, tutorials.

Advantage of this organization is that users are directed naturally down
the sidebar from Intro -> Get started -> How-tos, which is roughly how
we expect them to proceed.

This also makes deployment info more accessible as it is highlighted in
the "Getting started" section.

![Screenshot 2025-01-14 at 1 37
01 PM](https://github.com/user-attachments/assets/76dcde25-874b-4def-ad71-5313e399b7d7)
2025-01-14 15:29:59 -05:00
ccurmeandGitHub ecfb3e3850 Merge branch 'main' into eugene/langgraph_nav 2025-01-14 15:21:26 -05:00
Chester Curme ec71cd6fc1 update 2025-01-14 15:09:46 -05:00
Chester Curme 2db6d9d7b5 clean up 2025-01-14 15:08:48 -05:00
Chester Curme 0f5df797af fix link 2025-01-14 15:00:38 -05:00
Chester Curme 5dc3f14e8b populate troubleshootings nav 2025-01-14 14:56:23 -05:00
Chester Curme f25ea9ecaf populate tutorials nav 2025-01-14 14:50:05 -05:00
Chester Curme 9940a9f833 populate how-tos nav 2025-01-14 14:47:42 -05:00
Chester Curme d175d5e4a8 populate concepts nav 2025-01-14 14:42:10 -05:00
Chester Curme fe63440d98 add deployment landing page 2025-01-14 14:40:45 -05:00
bracesproul 0d50c62283 cr 2025-01-14 10:39:57 -08:00
vbarda a9073241b2 add css for navbar depth 2025-01-14 13:31:39 -05:00
Chester Curme 0a04262f05 add custom css 2025-01-14 12:46:00 -05:00
Chester Curme 381796c474 hack 2025-01-14 12:39:38 -05:00
Chester Curme 09ae5ce05d update 2025-01-14 11:26:49 -05:00
Chester Curme 59e7751a8c update 2025-01-14 10:48:50 -05:00
Chester Curme b9816e31ff update 2025-01-13 19:12:51 -05:00
Chester Curme 3ea7f9469a update 2025-01-13 18:26:24 -05:00
Chester Curme 24e61fafa9 update 2025-01-13 16:22:58 -05:00
bracesproul b52b32b38e format n lint 2025-01-13 13:14:36 -08:00
Brace SproulandGitHub 5aefa5dc8c Merge branch 'main' into brace/interrupt-schema 2025-01-13 13:08:37 -08:00
Chester Curme 2b370cf3b7 template applications -> lg plat 2025-01-13 14:21:12 -05:00
Chester Curme 839bea0f33 fixup 2025-01-13 14:16:14 -05:00
bracesproul cfb121ee8f cr 2025-01-10 10:45:45 -08:00
bracesproul d27beeed18 move to prebuilt 2025-01-10 10:45:22 -08:00
Eugene Yurtsev fba5ca7ab9 x 2025-01-09 12:24:30 -05:00
Eugene Yurtsev f141089811 x 2025-01-09 11:11:04 -05:00
Eugene Yurtsev 38487cb158 x 2025-01-09 11:11:04 -05:00
bracesproul 8534212a25 cr 2025-01-07 10:15:48 -08:00
bracesproul e39255a792 feat: Add interrupt schema to library 2025-01-07 10:00:38 -08:00
140 changed files with 7055 additions and 9224 deletions
+1 -16
View File
@@ -17,21 +17,11 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
core-version:
- "latest"
ff-send-v2:
- "false"
include:
- python-version: "3.11"
core-version: ">=0.2.42,<0.3.0"
- python-version: "3.11"
core-version: "latest"
ff-send-v2: "true"
defaults:
run:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }}, ff-send-v2: ${{ matrix.ff-send-v2 }})"
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
@@ -51,14 +41,9 @@ jobs:
shell: bash
run: |
poetry install --with dev
if [ "${{ matrix.core-version }}" != "latest" ]; then
poetry run pip install "langchain-core${{ matrix.core-version }}"
fi
- name: Run tests
shell: bash
env:
LANGGRAPH_FF_SEND_V2: ${{ matrix.ff-send-v2 }}
run: |
make test_parallel
+14 -6
View File
@@ -31,7 +31,6 @@ jobs:
"libs/cli",
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-duckdb",
"libs/checkpoint-postgres",
"libs/scheduler-kafka",
]
@@ -44,12 +43,12 @@ jobs:
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
working-directory: [
working-directory:
[
"libs/cli",
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-duckdb",
"libs/checkpoint-postgres"
"libs/checkpoint-postgres",
]
uses: ./.github/workflows/_test.yml
with:
@@ -76,7 +75,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"
- name: Run check_sdk_methods script
run: python .github/scripts/check_sdk_methods.py
@@ -133,7 +132,16 @@ jobs:
ci_success:
name: "CI Success"
needs: [lint, lint-js, test, test-langgraph, test-scheduler-kafka, integration-test, test-js]
needs:
[
lint,
lint-js,
test,
test-langgraph,
test-scheduler-kafka,
integration-test,
test-js,
]
if: |
always()
runs-on: ubuntu-latest
+2
View File
@@ -86,6 +86,7 @@ jobs:
echo "Running link check on all HTML files matching notebooks in docs directory..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
@@ -107,6 +108,7 @@ jobs:
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "http://localhost:2024.*" \
--check-links-ignore "http://127.0.0.1:.*" \
+175 -82
View File
@@ -12,25 +12,48 @@
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
### Why use LangGraph?
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
LangGraph provides fine-grained control over both the flow and state of your
agent applications. It implements a central
[persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/),
enabling features that are common to most agent architectures:
### Key Features
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
- **Cycles and Branching**: Implement loops and conditionals in your apps.
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
@@ -47,9 +70,7 @@ pip install -U langgraph
## Example
One of the central concepts of LangGraph is state. Each graph execution creates a state that is passed between nodes in the graph as they execute, and each node updates this internal state with its return value after it executes. The way that the graph updates its internal state is defined by either the type of graph chosen or a custom function.
Let's take a look at a simple example of an agent that can use a search tool.
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
```shell
pip install langchain-anthropic
@@ -66,10 +87,72 @@ export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_sk_...
```
```python
from typing import Annotated, Literal, TypedDict
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
<details open>
<summary>High-level implementation</summary>
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
tools = [search]
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
app = create_react_agent(model, tools, checkpointer=checkpointer)
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
> LangGraph is a **low-level** framework that allows you to implement any custom agent
architectures. Click on the low-level implementation below to see how to implement a
tool-calling agent from scratch.
<details>
<summary>Low-level implementation</summary>
```python
from typing import Literal
from langchain_core.messages import HumanMessage
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
@@ -91,7 +174,7 @@ tools = [search]
tool_node = ToolNode(tools)
model = ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0).bind_tools(tools)
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
# Define the function that determines whether to continue or not
def should_continue(state: MessagesState) -> Literal["tools", END]:
@@ -145,92 +228,102 @@ checkpointer = MemorySaver()
# Note that we're (optionally) passing the memory when compiling the graph
app = workflow.compile(checkpointer=checkpointer)
# Use the Runnable
# Use the agent
final_state = app.invoke(
{"messages": [HumanMessage(content="what is the weather in sf")]},
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
<b>Step-by-step Breakdown</b>:
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/modules/agents/tools/custom_tools">here</a>.
</li>
</ul>
</details>
```python
final_state = app.invoke(
{"messages": [HumanMessage(content="what about ny")]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<details>
<summary>Initialize graph with state.</summary>
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
### Step-by-step Breakdown
<details>
<summary>Define graph nodes.</summary>
1. <details>
<summary>Initialize the model and tools.</summary>
There are two main nodes we need:
- we use `ChatAnthropic` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
2. <details>
<summary>Initialize graph with state.</summary>
<details>
<summary>Define entry point and graph edges.</summary>
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
First, we need to set the entry point for graph execution - <code>agent</code> node.
3. <details>
<summary>Define graph nodes.</summary>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
There are two main nodes we need:
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
<details>
<summary>Compile the graph.</summary>
4. <details>
<summary>Define entry point and graph edges.</summary>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/v0.2/docs/concepts/#runnable-interface">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
First, we need to set the entry point for graph execution - `agent` node.
<details>
<summary>Execute the graph.</summary>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
- Conditional edge: after the agent is called, we should either:
- a. Run tools if the agent said to take an action, OR
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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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
@@ -1 +1 @@
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
@@ -1 +0,0 @@
eNqVVnlUFEcaxyMa9xl312jcEI9yPFhdemCYAQHjQQBlNhEQRsFzbLprZhp6uts+gBFNVoxXYkgajUey68UAOiqCEo+4vIerEVGTF2WjIuhGhUWjYsxzNV7rVvXMAB7PZHn8MV311Xf8vt/3q8ovzYaixPBcp+0MJ0ORpGT0Ian5pSKcq0BJfr/ECWUHT7uTk1ItRYrI1A9zyLIgRYeEkAKjJznZIfICQ+kp3hmSbQhxQkki7VByZ/C061x3W57OSeZaZT4LcpIuGhhCw0zBQOe3Qisz8nQiz0L0S6dIUNShXYpHqXAyXjIDDkIaSLwTApgrQFEGdoWhSY6CwMaLIENhWJrh7EB2MBKIMQPklZP1IJZXWBq4eAX46gCkJDGS3HbQCcfpFgSD9uD+ffnJDFB6MszVcolF0UmGY12DgRkoHI2AQ/Y0Ck3KWigt1RdmSXIdcrTglNE/CRB2AgtzgYyR9PrL0QrIgBy0MTKwibwTSAKkGJJl5qEoWRyfw0LaDvXAHMSyAEGHzkGQ4q02pkOxCqf1FDkHUKJIlpQhzlZsQwZtoLx8eec4eEChT0HksxkaAicvQkBDVDgKB3C5JEUpInbCcKg2J4md6zFoskvQkNTw0rBtW+F51opSxFYc6dTWnskUbzKcoGCsUVu829jSgurC1NDwlf4vgKELkCh/nzPNoB0ZVLcdys+HFWDAcH+eaI0el6VjaH9NijXUkBw/bYpdlt8W3sqcmvVOYnj41Ox4Mk23YNYT7HqW2jOegkeEksI+RT5dGgzW2uqtWdKKcUBRy90BWWEwSINBNKoPpemEnJfxlANSWYBXZPAOydkniqTgwPYaRt7O+/Axy0EScCqUw9tlEbIMmcFCrcuoh2hiGfyJ+MgBicE4AFKReY538orkdSJ5O+/rr/VXQYMhlKxQFHkRWdtIVoIIrVlYFngastgDxZIKDQkjEU5IPMdBmQhDwhEaERbqD+eTDj+bZDKbYV1WCZIi5fBhKVkz0WF8gEbEFxkBMxUbxwCvHYCcneFQr9GOU+s/5hLuuAgduPpsBL+f7sEaLLKoSDLEiGsR9GCKBG0Kqx3MQWfadUCbKikH0dbLPaSqgMzAXUH+RAQdgNkagMCMSQ8kh2/ikR740kMHRZe+bS6sEuqsk9TGA40nZgSjSWieTrPUfj1VakdPOCWW57OAInQcV0kW0WDoFmBq40FhRIi7OMPndFYHUz4jE1KyZpr34kF+Ko14v/BgMiN+40uH9KtSm/hoaHrHjrFpSGK+K5xGSmyohQA0ypCSWRdAmCM72S8PvuQlICmCwCOJyIAunvNRHvl3MhK+6ST9TG4mZ+F9momC+SUyGI8A6WrziKYjyCcdQegK6jCJT0gkXhYZu6Ndk35Vz9oVLu+Xe+E3fm43Zi0odUAS30cfux28JKsVz1zNOxGNoSATkKN4LJTqDvs8RghGym7DffFQeMo0EFRPFoQCgQQxG5Z4T6nlpCCwDKV1LAQP1XafRhE4l2e3PRg9QhMIdW+MP4+QZBd6SXAgVG806cPKcwlJu07RU4BgkVCpJYK2f6DjhkBSWcgP4XulqCXew2UdbXhJLZ5EUkmpT7jEvFeLSdEZYdrdcV1UODTtUC2NTX42nG+zPZxRbzDooyqecCy5OEot1nSrog3ktiMepFRGIjSCCDWU+VFikdDIDtVtiIwybUEMFRAL4aIS5FNWpHw3agk8cbTU9yjanPS2v50XAnq741B71CqLQwkGYREgFQoASyEwjIo2GaPRysRJlu2xvjiW53ajwiIiJbKhjsT7u19KORQuC9Ke2Of2vV7XXhfWPhbJo0z4OIi6hT9Vtyk0NLR++AstRcR9hsMR3caoqKhf8IuQgbJaiesjQqOIsAiLt8pw0/R68LyT3melL58SnA/KaOgLLNvz8VuDF1o/P5+wiOkeX9IEQ6t/R7/RbTfKlJgKTZYJk9IpZ2RmXEp2giEjPOGLXIJieYUmZPS2hoTGiFxZrQdUBDTQNElTkTYqMpy2GSOibBE0CkUbbUbDqKiibIZUPQa9Adh53s7CnbETiFgSqQmRqtFGLY2blhgzyRy7PZ1I4TN4hJ+FRDhzPAdLUqGI+Kh6tNBowkVYgo6nxExTKyOpqPBQaIqMhJTRZIwMJ+LTUsr9BGojiBvLg/aGX1jiFaWvuiwc9OHLAdpfl5lTrPzliJ6PR74/+35ht6rKAzd+Cuu5lPyTkLS011d7er005GREvxly/MjlabcPDBp6gOGTlhAfNN387VV7zUHn+Y03Ln+39POvTY1Jg//VcGbcuEc/1/w35NyVuve4ZjZwa5w6ck/TVL3ZOnbjyG2kaa+4qOsP50/fGyhtPH7t+CD9lIyhBxpnOrmysVxZ4zHX8gknG+/cbDnjnlAqVo3d01p2aN9Sc5ip8Jjx4LIjfcbsuHjn0vhPttQs/8f81VdGrAxcO95VXJdQW/3y0r5zes4ZMKn51Nqm6oWdt/Ts2r/+D3kOY/ft694rjev3m5peN+ZPlBoKAxffiJ2/LGcAcbVvb9fr3Tz3TmburTo6MPKz6n4rbvA9asp0Z2Frl5ps7u6aGdWrN++4WFbUo9sHR3q3BPN30zuRn1gqwlrnedKv0H0+Pb5txX/WryEPx22qTht4pPfaSZt7Fipl7munHubsz7uQOq7TWyuFjUXuMQn563N61HVduKohfW1grXRo4IN3PcnzdZWd4+H19I/GBC42Dp91aEengiHkqZEJdcmde3ZfsYQYIXQtMv0xIXXEo/IlO/uCu2zda63RU38sfBDQhfzriORtXVesCjTHTt5WePJbtXdT+tBbxR8NUHLnmL84cZced/jrwHdnnc2zZk1OfxxSfWm1LraubmtKQW0O20QEfXa54mbzlikLXgUW/fYlAWrBt79bnUD8s1fvfev+kri48mz+4cLpLfohk5M3Lrrd3TP9fI9K+qWckfsK2AHpV+9cjw6+tDPz4sKGb75fVrGi+o2tfy4+Pypuxs8pD6dZVpWQ1hODDrdkVA/7HK48fPbam5HFlgL21aEptbUrT9furne2nDDu32Af0rmhcWd6mjvkunV90Pfs3h8K+ncp3jW6c/mIpqNJ6+ZMVAPLyqpmH2opeNgYfqTHkTjYOK3eNltJKdrUGpBoKXe88s30Y1X8LdujCQF/K088trbJ+tNUIxz245bKyE3jJw/cV2OZPkYf++7kVUmpZq7zldEl8bsfZJ77sntu0odz19zrVPDdfiVjbtC9qrrXHOn5r7hdDf1aKu6/zh2k3tz55fDSdefh3Xn5w2uTT592xZjzC+7PvVDU/9C1CNeUXa399dErrp9pbd7waerKW4bfe+L2Kn0OFTX3OS9uLM693bTHcPfRseb7g86OHV1A7Zm4cky80vDxhrK0xF1Vj/fcu/rv5uY30Kw9ftwlYL57yW57t4CA/wFNCDta
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -0,0 +1 @@
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
@@ -1 +0,0 @@
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dN972SP/KqafcCW9MfE3e7f+y8b/agwuPrf37JoXputubDp2cXRnK78szlot3gsJKa54ecmXn2yfW71julATevviRzk749etHtlSU3/0tHpS3ow/hWdTq+NTnh58ZfvBBUcvV2Scb7XMulGouim9cm/JjtemDS4Lm9dOhpUFqy/QE4aWHth8+IXNrLrlyvHpSftGzW3JuRr6vPrukszN9W3n7780L7Jl3vKJu5fJdZP2mcP36ytafzV/6TNTr7boznakvFl3P+FmVd6FKHTixOmnli2aGLvwcfG1r5/6+pru1/unsi0t8SPOCM8NWjG7zDMpJl/IO1UlPl13y6ie88m1X/zuXJr3sPd6rnfWv7LPTDk5NNET/Oz6vPhR3pikTW1LY1fn15wbFrucP/jGrmOJFaPj7rau2t+2Lazzb0uIKxltBktO+5o11lGbcuhPf9MUwlfeqfQ0ndplLLAkhjVrynYV76yakHs9c4Jq43n0JpeZ39naOu7K4Huc0FV2b7KFzTftfL3xyFTh28qy48NHVH34BZU0Zu/18ubItcIo/esj04QLKbrfLvuo7kTVZeFgRNWuNUz3rZduP/PhgVub9r186bhj6QJNZ1n6YbrEYjBHR68YrU84f+jFjleiN5fbn18+fRp/1jV+2JBEj22C6XJqyHMxacKgqe6Smux3pSrZ9sX0vJSMisrThk+m1rV3XHr30j9s3zbtOfPayqB/50fMiQu/9qv9Xo1pQor65pg5jqYRHwimr70zPv7Fhajww8MmhBo7m56d0uXYPbJpzKmPT78VnHF9wbX6/dnZ+z7fM8s4/oNV42+v2RD+t5kbgqdHRL46TD3qzu0zmYd2hsXMP998PPWnUds+GXuySWe8eeRA16Vka35428kdf9gxpFqK3WkbN7/b+nrNOxvHxlJznz+5YHJR67MH15lD54dGu375VE7o+49v23px57X2nMoZk/Z+dOeEaU77lht7NkXPGlRTfvq/g8gtd8k5fxLWuis+Gzvee2jrwb9n5r4dX/DmXmP2qpXVh9p19cW3E7+4ZV+Xs8rwx3m59J3Clm/ylhtWl27/pSml4UjE8WNdK2e9UN59496f+c//MSQo6P79IUHd11LTXSOCgv4XTCOo8w==
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -1 +0,0 @@
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
@@ -561,6 +561,16 @@
},
"resource": {
"$ref": "#/components/schemas/ResourceService"
},
"status": {
"type": "string",
"enum": [
"AWAITING_DATABASE",
"READY",
"AWAITING_DELETE",
"UNKNOWN"
],
"description": "Deployment status of the project.\n\nNon-terminal statuses: `AWAITING_DATABASE`, `AWAITING_DELETE`. All other statuses are terminal."
}
}
},
+372 -212
View File
@@ -4,20 +4,26 @@ The LangGraph command line interface includes commands to build and run a LangGr
## Installation
1. Ensure that Docker is installed (e.g. `docker --version`).
2. Install the `langgraph-cli` package:
=== "pip"
```bash
pip install langgraph-cli
```
1. Ensure that Docker is installed (e.g. `docker --version`).
2. Install the CLI package:
=== "Homebrew (MacOS only)"
=== "Python"
```bash
pip install langgraph-cli
# Install via Homebrew
brew install langgraph-cli
```
3. Run the command `langgraph --help` to confirm that the CLI is installed.
=== "JS"
```bash
npx @langchain/langgraph-cli
# Install globally, will be available as `langgraphjs`
npm install -g @langchain/langgraph-cli
```
3. Run the command `langgraph --help` or `npx @langchain/langgraph-cli --help` to confirm that the CLI is working correctly.
[](){#langgraph.json}
@@ -25,17 +31,6 @@ The LangGraph command line interface includes commands to build and run a LangGr
The LangGraph CLI requires a JSON configuration file with the following keys:
| Key | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
| `auth` | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
| `env` | Path to `.env` file or a mapping from environment variable to its value. |
| `store` | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, meaningto index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
| `python_version` | `3.11` or `3.12`. Defaults to `3.11`. |
| `pip_config_file` | Path to `pip` config file. |
| `dockerfile_lines` | Array of additional lines to add to Dockerfile following the import from parent image. |
<div class="admonition tip">
<p class="admonition-title">Note</p>
<p>
@@ -43,253 +38,418 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
</p>
</div>
=== "Python"
| Key | Description |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <span style="white-space: nowrap;">`dependencies`</span> | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, which means to index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
| <span style="white-space: nowrap;">`python_version`</span> | `3.11` or `3.12`. Defaults to `3.11`. |
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
| <span style="white-space: nowrap;">`pip_config_file`</span> | Path to `pip` config file. |
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
=== "JS"
| Key | Description |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./src/graph.ts:variable`, where `variable` is an instance of `CompiledStateGraph`</li><li>`./src/graph.ts:makeGraph`, where `makeGraph` is a function that takes a config dictionary (`LangGraphRunnableConfig`) and creates an instance of `StateGraph` / `CompiledStateGraph`.</li></ul> |
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, which means to index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
### Examples
#### Basic Configuration
=== "Python"
#### Basic Configuration
```json
{
"dependencies": ["."],
"graphs": {
"chat": "./chat/graph.py:graph"
}
}
```
#### Adding semantic search to the store
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
The `fields` configuration determines which parts of your documents to embed:
- If omitted or set to `["$"]`, the entire document will be embedded
- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
- Documents missing specified fields will still be stored but won't have embeddings for those fields
- You can still override which fields to embed on a specific item at `put` time using the `index` parameter
```json
{
"dependencies": ["."],
"graphs": {
"memory_agent": "./agent/graph.py:graph"
},
"store": {
"index": {
"embed": "openai:text-embedding-3-small",
"dims": 1536,
"fields": ["$"]
```json
{
"dependencies": ["."],
"graphs": {
"chat": "./chat/graph.py:graph"
}
}
}
}
```
```
!!! note "Common model dimensions"
- openai:text-embedding-3-large: 3072
- openai:text-embedding-3-small: 1536
- openai:text-embedding-ada-002: 1536
- cohere:embed-english-v3.0: 1024
- cohere:embed-english-light-v3.0: 384
- cohere:embed-multilingual-v3.0: 1024
- cohere:embed-multilingual-light-v3.0: 384
#### Adding semantic search to the store
#### Semantic search with a custom embedding function
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
The `fields` configuration determines which parts of your documents to embed:
```json
{
"dependencies": ["."],
"graphs": {
"memory_agent": "./agent/graph.py:graph"
},
"store": {
"index": {
"embed": "./embeddings.py:embed_texts",
"dims": 768,
"fields": ["text", "summary"]
}
}
}
```
- If omitted or set to `["$"]`, the entire document will be embedded
- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
- Documents missing specified fields will still be stored but won't have embeddings for those fields
- You can still override which fields to embed on a specific item at `put` time using the `index` parameter
The `embed` field in store configuration can reference a custom function that takes a list of strings and returns a list of embeddings. Example implementation:
```python
# embeddings.py
def embed_texts(texts: list[str]) -> list[list[float]]:
"""Custom embedding function for semantic search."""
# Implementation using your preferred embedding model
return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors
```
#### Adding custom authentication
```json
{
"dependencies": ["."],
"graphs": {
"chat": "./chat/graph.py:graph"
},
"auth": {
"path": "./auth.py:auth",
"openapi": {
"securitySchemes": {
"apiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key"
}
```json
{
"dependencies": ["."],
"graphs": {
"memory_agent": "./agent/graph.py:graph"
},
"security": [
{"apiKeyAuth": []}
]
},
"disable_studio_auth": false
}
}
```
"store": {
"index": {
"embed": "openai:text-embedding-3-small",
"dims": 1536,
"fields": ["$"]
}
}
}
```
!!! note "Common model dimensions"
- `openai:text-embedding-3-large`: 3072
- `openai:text-embedding-3-small`: 1536
- `openai:text-embedding-ada-002`: 1536
- `cohere:embed-english-v3.0`: 1024
- `cohere:embed-english-light-v3.0`: 384
- `cohere:embed-multilingual-v3.0`: 1024
- `cohere:embed-multilingual-light-v3.0`: 384
#### Semantic search with a custom embedding function
If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
```json
{
"dependencies": ["."],
"graphs": {
"memory_agent": "./agent/graph.py:graph"
},
"store": {
"index": {
"embed": "./embeddings.py:embed_texts",
"dims": 768,
"fields": ["text", "summary"]
}
}
}
```
The `embed` field in store configuration can reference a custom function that takes a list of strings and returns a list of embeddings. Example implementation:
```python
# embeddings.py
def embed_texts(texts: list[str]) -> list[list[float]]:
"""Custom embedding function for semantic search."""
# Implementation using your preferred embedding model
return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors
```
#### Adding custom authentication
```json
{
"dependencies": ["."],
"graphs": {
"chat": "./chat/graph.py:graph"
},
"auth": {
"path": "./auth.py:auth",
"openapi": {
"securitySchemes": {
"apiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key"
}
},
"security": [{ "apiKeyAuth": [] }]
},
"disable_studio_auth": false
}
}
```
See the [authentication conceptual guide](../../concepts/auth.md) for details, and the [setting up custom authentication](../../tutorials/auth/getting_started.md) guide for a practical walk through of the process.
=== "JS"
#### Basic Configuration
```json
{
"graphs": {
"chat": "./src/graph.ts:graph"
}
}
```
See the [authentication conceptual guide](../../concepts/auth.md) for details, and the [setting up custom authentication](../../tutorials/auth/getting_started.md) guide for a practical walk through of the process.
## Commands
The base command for the LangGraph CLI is `langgraph`.
**Usage**
```
langgraph [OPTIONS] COMMAND [ARGS]
```
=== "Python"
The base command for the LangGraph CLI is `langgraph`.
```
langgraph [OPTIONS] COMMAND [ARGS]
```
=== "JS"
The base command for the LangGraph.js CLI is `langgraphjs`.
```
npx @langchain/langgraph-cli [OPTIONS] COMMAND [ARGS]
```
We recommend using `npx` to always use the latest version of the CLI.
### `dev`
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
=== "Python"
!!! note "Python only"
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
Currently, the CLI only supports Python >= 3.11.
JS support is coming soon.
!!! note
**Installation**
Currently, the CLI only supports Python >= 3.11.
This command requires the "inmem" extra to be installed:
**Installation**
```bash
pip install -U "langgraph-cli[inmem]"
```
This command requires the "inmem" extra to be installed:
**Usage**
```bash
pip install -U "langgraph-cli[inmem]"
```
```
langgraph dev [OPTIONS]
```
**Usage**
**Options**
```
langgraph dev [OPTIONS]
```
| Option | Default | Description |
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
| `--port INTEGER` | `2024` | Port to bind the server to |
| `--no-reload` | | Disable auto-reload |
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
| `--no-browser` | | Disable automatic browser opening |
| `--debug-port INTEGER` | | Port for debugger to listen on |
| `--help` | | Display command documentation |
**Options**
| Option | Default | Description |
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
| `--port INTEGER` | `2024` | Port to bind the server to |
| `--no-reload` | | Disable auto-reload |
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
| `--debug-port INTEGER` | | Port for debugger to listen on |
| `--help` | | Display command documentation |
=== "JS"
Run LangGraph API server in development mode with hot reloading capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
**Usage**
```
npx @langchain/langgraph-cli dev [OPTIONS]
```
**Options**
| Option | Default | Description |
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
| `--port INTEGER` | `2024` | Port to bind the server to |
| `--no-reload` | | Disable auto-reload |
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
| `--debug-port INTEGER` | | Port for debugger to listen on |
| `--help` | | Display command documentation |
### `build`
Build LangGraph Cloud API server Docker image.
=== "Python"
**Usage**
Build LangGraph Cloud API server Docker image.
```
langgraph build [OPTIONS]
```
**Usage**
**Options**
```
langgraph build [OPTIONS]
```
**Options**
| Option | Default | Description |
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `--help` | | Display command documentation. |
=== "JS"
Build LangGraph Cloud API server Docker image.
**Usage**
```
npx @langchain/langgraph-cli build [OPTIONS]
```
**Options**
| Option | Default | Description |
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
| `--no-pull` | | Use locally built images. Defaults to `false` to build with latest remote Docker image. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `--help` | | Display command documentation. |
| Option | Default | Description |
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `--help` | | Display command documentation. |
### `up`
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
=== "Python"
**Usage**
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
```
langgraph up [OPTIONS]
```
**Usage**
**Options**
```
langgraph up [OPTIONS]
```
| Option | Default | Description |
| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--wait` | | Wait for services to start before returning. Implies --detach |
| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
| `--watch` | | Restart on file changes |
| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
| `--verbose` | | Show more output from the server logs. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
| `--help` | | Display command documentation. |
**Options**
| Option | Default | Description |
| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--wait` | | Wait for services to start before returning. Implies --detach |
| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
| `--watch` | | Restart on file changes |
| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
| `--verbose` | | Show more output from the server logs. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
| `--help` | | Display command documentation. |
=== "JS"
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
**Usage**
```
npx @langchain/langgraph-cli up [OPTIONS]
```
**Options**
| Option | Default | Description |
| ---------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| <span style="white-space: nowrap;">`--wait`</span> | | Wait for services to start before returning. Implies --detach |
| <span style="white-space: nowrap;">`--postgres-uri TEXT`</span> | Local database | Postgres URI to use for the database. |
| <span style="white-space: nowrap;">`--watch`</span> | | Restart on file changes |
| <span style="white-space: nowrap;">`-c, --config FILE`</span> | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| <span style="white-space: nowrap;">`-d, --docker-compose FILE`</span> | | Path to docker-compose.yml file with additional services to launch. |
| <span style="white-space: nowrap;">`-p, --port INTEGER`</span> | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| <span style="white-space: nowrap;">`--no-pull`</span> | | Use locally built images. Defaults to `false` to build with latest remote Docker image. |
| <span style="white-space: nowrap;">`--recreate`</span> | | Recreate containers even if their configuration and image haven't changed |
| <span style="white-space: nowrap;">`--help`</span> | | Display command documentation. |
### `dockerfile`
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
=== "Python"
**Usage**
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
```
langgraph dockerfile [OPTIONS] SAVE_PATH
```
**Usage**
**Options**
```
langgraph dockerfile [OPTIONS] SAVE_PATH
```
| Option | Default | Description |
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
| `--help` | | Show this message and exit. |
**Options**
Example:
| Option | Default | Description |
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
| `--help` | | Show this message and exit. |
```bash
langgraph dockerfile -c langgraph.json Dockerfile
```
Example:
This generates a Dockerfile that looks similar to:
```bash
langgraph dockerfile -c langgraph.json Dockerfile
```
```dockerfile
FROM langchain/langgraph-api:3.11
This generates a Dockerfile that looks similar to:
ADD ./pipconf.txt /pipconfig.txt
```dockerfile
FROM langchain/langgraph-api:3.11
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
ADD ./pipconf.txt /pipconfig.txt
ADD ./graphs /deps/__outer_graphs/src
RUN set -ex && \
for line in '[project]' \
'name = "graphs"' \
'version = "0.1"' \
'[tool.setuptools.package-data]' \
'"*" = ["**/*"]'; do \
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
done
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ADD ./graphs /deps/__outer_graphs/src
RUN set -ex && \
for line in '[project]' \
'name = "graphs"' \
'version = "0.1"' \
'[tool.setuptools.package-data]' \
'"*" = ["**/*"]'; do \
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
done
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
```
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
???+ note "Updating your langgraph.json file"
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
```
???+ note "Updating your langgraph.json file"
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
=== "JS"
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
**Usage**
```
npx @langchain/langgraph-cli dockerfile [OPTIONS] SAVE_PATH
```
**Options**
| Option | Default | Description |
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
| `--help` | | Show this message and exit. |
Example:
```bash
npx @langchain/langgraph-cli dockerfile -c langgraph.json Dockerfile
```
This generates a Dockerfile that looks similar to:
```dockerfile
FROM langchain/langgraphjs-api:20
ADD . /deps/agent
RUN cd /deps/agent && yarn install
ENV LANGSERVE_GRAPHS='{"agent":"./src/react_agent/graph.ts:graph"}'
WORKDIR /deps/agent
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts
```
???+ note "Updating your langgraph.json file"
The `npx @langchain/langgraph-cli dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
+1
View File
@@ -57,6 +57,7 @@ With a Self-Hosted Lite deployment, you are responsible for managing the infrast
Youll build a Docker image using the [LangGraph CLI](./langgraph_cli.md), which can then be deployed on your own infrastructure.
[Cron jobs](../cloud/how-tos/cron_jobs.md) are not available for Self-Hosted Lite deployments.
For more information, please see:
+2 -4
View File
@@ -1,6 +1,4 @@
---
hide:
- navigation
title: Concepts
description: Conceptual Guide for LangGraph
---
@@ -15,11 +13,11 @@ The conceptual guide does not cover step-by-step instructions or specific implem
## LangGraph
**High Level**
### High Level
- [Why LangGraph?](high_level.md): A high-level overview of LangGraph and its goals.
**Concepts**
### Concepts
- [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.
+13 -5
View File
@@ -6,21 +6,29 @@
## Overview
LangGraph's Cloud SaaS is a managed service for deploying LangGraph APIs, regardless of its definition or dependencies. The service offers managed implementations of checkpointers and stores, allowing you to focus on building the right cognitive architecture for your use case. By handling scalable & secure infrastructure, LangGraph Cloud offers the fastest path to getting your LangGraph API deployed to production.
LangGraph's Cloud SaaS is a managed service for deploying LangGraph Servers, regardless of its definition or dependencies. The service offers managed implementations of checkpointers and stores, allowing you to focus on building the right cognitive architecture for your use case. By handling scalable & secure infrastructure, LangGraph Cloud SaaS offers the fastest path to getting your LangGraph Server deployed to production.
## Deployment
A **deployment** is an instance of a LangGraph API. A single deployment can have many [revisions](#revision). When a deployment is created, all the necessary infrastructure (e.g. database, containers, secrets store) are automatically provisioned. See the [architecture diagram](#architecture) below for more details.
A **deployment** is an instance of a LangGraph Server. A single deployment can have many [revisions](#revision). When a deployment is created, all the necessary infrastructure (e.g. database, containers, secrets store) are automatically provisioned. See the [architecture diagram](#architecture) below for more details.
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
## Resource Allocation
Resource Allocation:
| **Deployment Type** | **CPU** | **Memory** | **Scaling** |
|---------------------|---------|------------|---------------------|
| Development | 1 CPU | 1 GB | Up to 1 container |
| Production | 2 CPU | 2 GB | Up to 10 containers |
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
## Persistence
A dedicated database is automatically created for each deployment. The database serves as the [persistence layer](../concepts/persistence.md) for the deployment.
When defining a graph to be deployed to LangGraph Cloud SaaS, a [checkpointer](../concepts/persistence.md#checkpointer-libraries) should not be configured by the user. Instead, a checkpointer is automatically configured for the graph.
There is no direct access to the database. All access to the database occurs through the LangGraph Server APIs.
## 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...
+12 -1
View File
@@ -60,7 +60,18 @@ LangGraph Studio (desktop) requires Docker Desktop version 4.24 or higher. Pleas
#### Configuration or environment issues
Another reason your project might fail to start is because your configuration file is defined incorrectly, or you are missing required environment variables.
Another reason your project might fail to start is because your configuration file is defined incorrectly, or you are missing required environment variables.
!!! Important "Note (desktop only)"
LangGraph Studio Desktop automatically populates `LANGCHAIN_*` environment variables for license verification and tracing, regardless of the contents of the `.env` file. All other environment variables defined in `.env` will be read as normal.
#### Incorrect data region (desktop only)
If you receive a license verification error when attempting to start the LangGraph Server, you may be logged into the incorrect LangSmith data region. Ensure that you're logged into the correct LangSmith data region and ensure that the LangSmith account has access to LangGraph platform.
1. In the top right-hand corner, click the user icon and select `Logout`.
1. At the login screen, click the `Data Region` dropdown menu and select the appropriate data region. Then click `Login to LangSmith`.
### How does interrupt work?
+19
View File
@@ -359,6 +359,25 @@ Use `Command` when you need to **both** update the graph state **and** route to
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
### Navigating to a node in a parent graph
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
return Command(
update={"foo": "bar"},
goto="other_subgraph", # where `other_subgraph` is a node in the parent graph
graph=Command.PARENT
)
```
!!! note
Setting `graph` to `Command.PARENT` will navigate to the closest parent graph.
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
### 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:
File diff suppressed because one or more lines are too long
@@ -23,12 +23,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li> \n",
@@ -368,7 +368,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -5,7 +5,7 @@
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to add memory to the prebuilt ReAct agent\n",
"# How to add thread-level memory to a ReAct Agent\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
@@ -28,12 +28,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
@@ -285,7 +285,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -0,0 +1,287 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to return structured output from the prebuilt ReAct agent\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Agent Architectures](../../concepts/agentic_concepts/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
" - [Tools](https://python.langchain.com/docs/concepts/tools/)\n",
" - [Structured Output](https://python.langchain.com/docs/concepts/structured_outputs/)\n",
"\n",
"To return structured output from the prebuilt ReAct agent you can provide a `response_format` parameter with the desired output schema to [create_react_agent][langgraph.prebuilt.chat_agent_executor.create_react_agent]:\n",
"\n",
"```python\n",
"class ResponseFormat(BaseModel):\n",
" \"\"\"Respond to the user in this format.\"\"\"\n",
" my_special_output: str\n",
"\n",
"\n",
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify the schema for the structured output using `response_format` parameter\n",
" response_format=ResponseFormat\n",
")\n",
"```\n",
"\n",
"Prebuilt ReAct makes an additional LLM call at the end of the ReAct loop to produce a structured output response. Please see [this guide](../react-agent-structured-output) to learn about other strategies for returning structured outputs from a tool-calling agent."
]
},
{
"cell_type": "markdown",
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "87a00ce9",
"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": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"# Define the structured output schema\n",
"\n",
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"class WeatherResponse(BaseModel):\n",
" \"\"\"Respond to the user in this format.\"\"\"\n",
"\n",
" conditions: str = Field(description=\"Weather conditions\")\n",
"\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify the schema for the structured output using `response_format` parameter\n",
" response_format=WeatherResponse,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"Let's now test our agent:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"response = graph.invoke(inputs)"
]
},
{
"cell_type": "markdown",
"id": "50e273a0-fbdb-4eee-89ca-580fbfb52daf",
"metadata": {},
"source": [
"You can see that the agent output contains a `structured_response` key with the structured output conforming to the specified `WeatherResponse` schema, in addition to the message history under `messages` key."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "300748d4-0ed2-470d-8dbc-7c14231e73b8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"WeatherResponse(conditions='cloudy')"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response[\"structured_response\"]"
]
},
{
"cell_type": "markdown",
"id": "bd9e3487-2cec-44cf-9472-0a51eebeddff",
"metadata": {},
"source": [
"### Customizing prompt"
]
},
{
"cell_type": "markdown",
"id": "a608548d-77fc-4d7a-845c-32ae9ec0489a",
"metadata": {},
"source": [
"You might need to further customize the second LLM call for the structured output generation and provide a system prompt. To do so, you can pass a tuple (prompt, schema):"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d1386f99-ffd1-4b36-86ec-cabb3357d929",
"metadata": {},
"outputs": [],
"source": [
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify both the system prompt and the schema for the structured output\n",
" response_format=(\"Always return capitalized weather conditions\", WeatherResponse),\n",
")\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"response = graph.invoke(inputs)"
]
},
{
"cell_type": "markdown",
"id": "91f34991-b406-4fd2-a776-4dd03e3dc3dd",
"metadata": {},
"source": [
"You can verify that the structured response now contains a capitalized value:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "ba43a67f-127c-45e7-982c-a8210d97a3ed",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"WeatherResponse(conditions='Cloudy')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response[\"structured_response\"]"
]
}
],
"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
}
@@ -14,7 +14,7 @@
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li> \n",
" <a href=\"https://python.langchain.com/v0.1/docs/modules/model_io/concepts/#systemmessage\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/messages/#systemmessage\">\n",
" SystemMessage\n",
" </a>\n",
" </li>\n",
@@ -24,12 +24,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
@@ -223,7 +223,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
+4 -4
View File
@@ -5,7 +5,7 @@
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to use the prebuilt ReAct agent"
"# How to use the pre-built ReAct agent"
]
},
{
@@ -24,12 +24,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
@@ -292,7 +292,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -591,7 +591,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.10.4"
}
},
"nbformat": 4,
+23 -11
View File
@@ -1,6 +1,4 @@
---
hide:
- navigation
title: How-to Guides
description: How to accomplish common tasks in LangGraph
---
@@ -24,10 +22,10 @@ These how-to guides show how to achieve that controllability.
### Persistence
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (per-thread persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
- [How to add thread-level persistence to your graph](persistence.ipynb)
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
- [How to add thread-level persistence to a subgraph](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 use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
@@ -85,7 +83,10 @@ Other methods:
### Tool calling
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of
[chat model](https://python.langchain.com/docs/concepts/chat_models/) API that accepts
tool schemas, along with messages, as input and returns invocations of those tools as
part of the output message.
These how-to guides show common patterns for tool calling with LangGraph:
@@ -100,7 +101,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
[Subgraphs](../concepts/low_level.md#subgraphs) allow you to reuse an existing graph from another graph. These how-to guides show how to use subgraphs:
- [How to add and use subgraphs](subgraph.ipynb)
- [How to use subgraphs](subgraph.ipynb)
- [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)
@@ -116,7 +117,7 @@ See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for i
### State Management
- [How to use Pydantic model as state](state-model.ipynb)
- [How to use Pydantic model as graph state](state-model.ipynb)
- [How to define input/output schema for your graph](input_output_schema.ipynb)
- [How to pass private state between nodes inside the graph](pass_private_state.ipynb)
@@ -126,7 +127,7 @@ See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for i
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to add node retries](node-retries.ipynb)
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
- [How to force tool-calling agent to structure output](react-agent-structured-output.ipynb)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
@@ -139,13 +140,18 @@ One of the big benefits of LangGraph is that you can easily create your own agen
These guides show how to use the prebuilt ReAct agent:
- [How to create a ReAct agent](create-react-agent.ipynb)
- [How to add memory to a ReAct agent](create-react-agent-memory.ipynb)
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
- [How to add thread-level memory to a ReAct Agent](create-react-agent-memory.ipynb)
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
- [How to return structured output from a ReAct agent](create-react-agent-structured-output.ipynb)
- [How to add semantic search for long-term memory to a ReAct agent](memory/semantic-search.ipynb#using-in-create-react-agent)
Interested in further customizing the ReAct agent? This guide provides an
overview of its underlying implementation to help you customize for your own needs:
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
## LangGraph Platform
This section includes how-to guides for LangGraph Platform.
@@ -189,11 +195,17 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
[Assistants](../concepts/assistants.md) is a configured instance of a template.
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.AssistantsClient)
for supported endpoints and other details.
- [How to configure agents](../cloud/how-tos/configuration_cloud.md)
- [How to version assistants](../cloud/how-tos/assistant_versioning.md)
### Threads
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.ThreadsClient)
for supported endpoints and other details.
- [How to copy threads](../cloud/how-tos/copy_threads.md)
- [How to check status of your threads](../cloud/how-tos/check_thread_status.md)
+2 -1
View File
@@ -44,7 +44,8 @@
"...\n",
"```\n",
"\n",
"!!! info \"Setup\n",
"!!! info \"Setup\"",
"\n",
" You need to run `.setup()` once on your checkpointer to initialize the database before you can use it."
]
},
@@ -17,12 +17,12 @@
" </a>\n",
" </li> \n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#messages\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/messages\">\n",
" Messages\n",
" </a>\n",
" </li>\n",
@@ -375,7 +375,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -15,7 +15,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to return structured output with a ReAct style agent\n",
"# How to force tool-calling agent to structure output\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to use Pydantic model as state\n",
"# How to use Pydantic model as graph state\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "176e8dbb-1a0a-49ce-a10e-2417e8ea17a0",
"metadata": {},
"source": [
"# How to add thread-level persistence to subgraphs"
"# How to add thread-level persistence to a subgraph"
]
},
{
+1 -1
View File
@@ -9,7 +9,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to add and use subgraphs\n",
"# How to use subgraphs\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
-2
View File
@@ -1,7 +1,5 @@
---
hide_comments: true
hide:
- navigation
title: Home
---
+5
View File
@@ -0,0 +1,5 @@
::: langgraph.func
options:
members:
- task
- entrypoint
@@ -6,7 +6,7 @@ support it.
One way this can occur is if you are using a [fanout](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)
or other parallel execution in your graph and you have defined a graph like this:
```python
```python hl_lines="2"
class State(TypedDict):
some_key: str
@@ -31,7 +31,7 @@ there is uncertainty around how to update the internal state.
To get around this, you can define a reducer that combines multiple values:
```python
```python hl_lines="5-6"
import operator
from typing import Annotated
+18
View File
@@ -0,0 +1,18 @@
# Deployment
Get started deploying your LangGraph applications locally or on the cloud with
[LangGraph Platform](../concepts/langgraph_platform.md).
## Get Started 🚀 {#quick-start}
- [LangGraph Server Quickstart](../tutorials/langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using REST API and LangGraph Studio Web UI.
- [LangGraph Template Quickstart](../concepts/template_applications.md): Start building with LangGraph Platform using a template application.
- [Deploy with LangGraph Cloud Quickstart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
## Deployment Options
- [Self-Hosted Lite](../concepts/self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
- [Cloud SaaS](../concepts/langgraph_cloud.md): Hosted as part of LangSmith.
- [Bring Your Own Cloud](../concepts/bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
- [Self-Hosted Enterprise](../concepts/self_hosted.md): Completely managed by you.
-2
View File
@@ -1,6 +1,4 @@
---
hide:
- navigation
title: Tutorials
---
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
# QuickStart: Launch Local LangGraph Server
# Quickstart: Launch Local LangGraph Server
This is a quick start guide to help you get a LangGraph app up and running locally.
@@ -250,4 +250,4 @@ Access detailed documentation for development and API usage:
- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the JS/TS SDK API Reference.
@@ -83,7 +83,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 2,
"id": "f04c6778-403b-4b49-9b93-678e910d5cec",
"metadata": {},
"outputs": [],
@@ -126,7 +126,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 3,
"id": "df2bd80b-c477-4d74-8faa-1c0548622239",
"metadata": {},
"outputs": [],
@@ -162,7 +162,11 @@
"llm = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"\n",
"def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
"class State(MessagesState):\n",
" next: str\n",
"\n",
"\n",
"def supervisor_node(state: State) -> Command[Literal[*members, \"__end__\"]]:\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": system_prompt},\n",
" ] + state[\"messages\"]\n",
@@ -171,7 +175,7 @@
" if goto == \"FINISH\":\n",
" goto = END\n",
"\n",
" return Command(goto=goto)"
" return Command(goto=goto, update={\"next\": goto})"
]
},
{
@@ -201,7 +205,7 @@
")\n",
"\n",
"\n",
"def research_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def research_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = research_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -217,7 +221,7 @@
"code_agent = create_react_agent(llm, tools=[python_repl_tool])\n",
"\n",
"\n",
"def code_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def code_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = code_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -229,7 +233,7 @@
" )\n",
"\n",
"\n",
"builder = StateGraph(MessagesState)\n",
"builder = StateGraph(State)\n",
"builder.add_edge(START, \"supervisor\")\n",
"builder.add_node(\"supervisor\", supervisor_node)\n",
"builder.add_node(\"researcher\", research_node)\n",
@@ -293,6 +293,10 @@
"from langchain_core.messages import HumanMessage, trim_messages\n",
"\n",
"\n",
"class State(MessagesState):\n",
" next: str\n",
"\n",
"\n",
"def make_supervisor_node(llm: BaseChatModel, members: list[str]) -> str:\n",
" options = [\"FINISH\"] + members\n",
" system_prompt = (\n",
@@ -308,7 +312,7 @@
"\n",
" next: Literal[*options]\n",
"\n",
" def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
" def supervisor_node(state: State) -> Command[Literal[*members, \"__end__\"]]:\n",
" \"\"\"An LLM-based router.\"\"\"\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": system_prompt},\n",
@@ -318,7 +322,7 @@
" if goto == \"FINISH\":\n",
" goto = END\n",
"\n",
" return Command(goto=goto)\n",
" return Command(goto=goto, update={\"next\": goto})\n",
"\n",
" return supervisor_node"
]
@@ -358,7 +362,7 @@
"search_agent = create_react_agent(llm, tools=[tavily_tool])\n",
"\n",
"\n",
"def search_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def search_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = search_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -374,7 +378,7 @@
"web_scraper_agent = create_react_agent(llm, tools=[scrape_webpages])\n",
"\n",
"\n",
"def web_scraper_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def web_scraper_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = web_scraper_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -410,7 +414,7 @@
},
"outputs": [],
"source": [
"research_builder = StateGraph(MessagesState)\n",
"research_builder = StateGraph(State)\n",
"research_builder.add_node(\"supervisor\", research_supervisor_node)\n",
"research_builder.add_node(\"search\", search_node)\n",
"research_builder.add_node(\"web_scraper\", web_scraper_node)\n",
@@ -528,7 +532,7 @@
")\n",
"\n",
"\n",
"def doc_writing_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def doc_writing_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = doc_writer_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -551,7 +555,7 @@
")\n",
"\n",
"\n",
"def note_taking_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def note_taking_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = note_taking_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -569,7 +573,7 @@
")\n",
"\n",
"\n",
"def chart_generating_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def chart_generating_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" result = chart_generating_agent.invoke(state)\n",
" return Command(\n",
" update={\n",
@@ -610,7 +614,7 @@
"outputs": [],
"source": [
"# Create the graph here\n",
"paper_writing_builder = StateGraph(MessagesState)\n",
"paper_writing_builder = StateGraph(State)\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",
@@ -730,7 +734,7 @@
},
"outputs": [],
"source": [
"def call_research_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def call_research_team(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" response = research_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
" return Command(\n",
" update={\n",
@@ -744,7 +748,7 @@
" )\n",
"\n",
"\n",
"def call_paper_writing_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
"def call_paper_writing_team(state: State) -> Command[Literal[\"supervisor\"]]:\n",
" response = paper_writing_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
" return Command(\n",
" update={\n",
@@ -759,7 +763,7 @@
"\n",
"\n",
"# Define the graph.\n",
"super_builder = StateGraph(MessagesState)\n",
"super_builder = StateGraph(State)\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",
+283 -269
View File
@@ -20,9 +20,9 @@ theme:
- content.action.edit
- content.tooltips
- header.autohide
- navigation.indexes
- navigation.expand
- navigation.footer
- navigation.indexes
- navigation.instant
- navigation.sections
- navigation.instant.prefetch
@@ -30,7 +30,6 @@ theme:
- navigation.path
- navigation.prune
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- navigation.tracking
- search.highlight
@@ -89,281 +88,296 @@ plugins:
options:
filters:
- "!^_"
nav:
- Home: index.md
- Tutorials:
- tutorials/index.md
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots:
- Chatbots: tutorials#chatbots
- tutorials/customer-support/customer-support.ipynb
- tutorials/chatbots/information-gather-prompting.ipynb
- tutorials/code_assistant/langgraph_code_assistant.ipynb
- RAG:
- RAG: tutorials#rag
- tutorials/rag/langgraph_adaptive_rag.ipynb
- tutorials/rag/langgraph_adaptive_rag_local.ipynb
- tutorials/rag/langgraph_agentic_rag.ipynb
- tutorials/rag/langgraph_crag.ipynb
- tutorials/rag/langgraph_crag_local.ipynb
- tutorials/rag/langgraph_self_rag.ipynb
- tutorials/rag/langgraph_self_rag_local.ipynb
- tutorials/sql-agent.ipynb
- Agent Architectures:
- Agent Architectures: tutorials#agent-architectures
- Multi-Agent Systems:
- Multi-Agent Systems: tutorials#multi-agent-systems
- tutorials/multi_agent/multi-agent-collaboration.ipynb
- tutorials/multi_agent/agent_supervisor.ipynb
- tutorials/multi_agent/hierarchical_agent_teams.ipynb
- Planning Agents:
- Planning Agents: tutorials#planning-agents
- tutorials/plan-and-execute/plan-and-execute.ipynb
- tutorials/rewoo/rewoo.ipynb
- tutorials/llm-compiler/LLMCompiler.ipynb
- Reflection & Critique:
- Reflection & Critique: tutorials#reflection-critique
- tutorials/reflection/reflection.ipynb
- tutorials/reflexion/reflexion.ipynb
- tutorials/tot/tot.ipynb
- tutorials/lats/lats.ipynb
- tutorials/self-discover/self-discover.ipynb
- Evaluation & Analysis:
- Evaluation & Analysis: tutorials#evaluation
- tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
- tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
- Experimental:
- Experimental: tutorials#experimental
- tutorials/storm/storm.ipynb
- tutorials/tnt-llm/tnt-llm.ipynb
- tutorials/web-navigation/web_voyager.ipynb
- tutorials/usaco/usaco.ipynb
- tutorials/extraction/retries.ipynb
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- tutorials/auth/getting_started.md
- tutorials/auth/resource_auth.md
- tutorials/auth/add_auth_server.md
- How-to Guides:
- how-tos/index.md
- LangGraph:
- LangGraph: how-tos#langgraph
- Controllability:
- Controllability: how-tos#controllability
- how-tos/branching.ipynb
- how-tos/map-reduce.ipynb
- how-tos/recursion-limit.ipynb
- how-tos/command.ipynb
- Persistence:
- Persistence: how-tos#persistence
- how-tos/persistence.ipynb
- how-tos/subgraph-persistence.ipynb
- how-tos/cross-thread-persistence.ipynb
- how-tos/persistence_postgres.ipynb
- how-tos/persistence_mongodb.ipynb
- how-tos/persistence_redis.ipynb
- Memory:
- Memory: how-tos#memory
- how-tos/memory/manage-conversation-history.ipynb
- how-tos/memory/delete-messages.ipynb
- how-tos/memory/add-summary-conversation-history.ipynb
- how-tos/memory/semantic-search.ipynb
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop
- how-tos/human_in_the_loop/breakpoints.ipynb
- how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
- how-tos/human_in_the_loop/edit-graph-state.ipynb
- how-tos/human_in_the_loop/wait-user-input.ipynb
- how-tos/human_in_the_loop/time-travel.ipynb
- how-tos/human_in_the_loop/review-tool-calls.ipynb
- Streaming:
- Streaming: how-tos#streaming
- how-tos/stream-values.ipynb
- how-tos/stream-updates.ipynb
- how-tos/streaming-tokens.ipynb
- how-tos/streaming-tokens-without-langchain.ipynb
- how-tos/streaming-content.ipynb
- how-tos/stream-multiple.ipynb
- how-tos/streaming-events-from-within-tools.ipynb
- how-tos/streaming-events-from-within-tools-without-langchain.ipynb
- how-tos/streaming-from-final-node.ipynb
- how-tos/streaming-subgraphs.ipynb
- how-tos/disable-streaming.ipynb
- Tool calling:
- Tool calling: how-tos#tool-calling
- 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:
- Subgraphs: how-tos#subgraphs
- how-tos/subgraph.ipynb
- how-tos/subgraphs-manage-state.ipynb
- how-tos/subgraph-transform-state.ipynb
- Multi-agent:
- Multi-agent: how-tos#multi-agent
- how-tos/agent-handoffs.ipynb
- how-tos/multi-agent-network.ipynb
- how-tos/multi-agent-multi-turn-convo.ipynb
- State Management:
- State Management: how-tos#state-management
- how-tos/state-model.ipynb
- how-tos/input_output_schema.ipynb
- how-tos/pass_private_state.ipynb
- Other:
- Other: how-tos#other
- how-tos/async.ipynb
- how-tos/visualization.ipynb
- how-tos/configuration.ipynb
- how-tos/node-retries.ipynb
- how-tos/react-agent-structured-output.ipynb
- how-tos/run-id-langsmith.ipynb
- how-tos/return-when-recursion-limit-hits.ipynb
- Prebuilt ReAct Agent:
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
- how-tos/create-react-agent.ipynb
- how-tos/create-react-agent-memory.ipynb
- how-tos/create-react-agent-system-prompt.ipynb
- how-tos/create-react-agent-hitl.ipynb
- how-tos/react-agent-from-scratch.ipynb
- LangGraph Platform:
- LangGraph Platform: how-tos#langgraph-platform
- Application Structure:
- Application Structure: how-tos#application-structure
- cloud/deployment/setup.md
- cloud/deployment/setup_pyproject.md
- cloud/deployment/setup_javascript.md
- cloud/deployment/semantic_search.md
- cloud/deployment/custom_docker.md
- cloud/deployment/test_locally.md
- cloud/deployment/graph_rebuild.md
- Deployment:
- Deployment: how-tos#deployment
- cloud/deployment/cloud.md
- how-tos/deploy-self-hosted.md
- how-tos/use-remote-graph.md
- Authentication & Access Control:
- Authentication & Access Control: how-tos#authentication-access-control
- cloud/how-tos/auth/custom_auth_new.md
- cloud/how-tos/auth/openapi_security_new.md
- Assistants:
- Assistants: how-tos#assistants
- cloud/how-tos/configuration_cloud.md
- cloud/how-tos/assistant_versioning.md
- Threads:
- Threads: how-tos#threads
- cloud/how-tos/copy_threads.md
- cloud/how-tos/check_thread_status.md
- Runs:
- Runs: how-tos#runs
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- Streaming:
- Streaming: how-tos#streaming_1
- cloud/how-tos/stream_values.md
- cloud/how-tos/stream_updates.md
- cloud/how-tos/stream_messages.md
- cloud/how-tos/stream_events.md
- cloud/how-tos/stream_debug.md
- cloud/how-tos/stream_multiple.md
nav:
- Home:
- Introduction: index.md
- Get started:
- Learn the basics: tutorials/introduction.ipynb
- Deployment:
- tutorials/deployment.md
- Local Deploy: tutorials/langgraph-platform/local-server.md
- Template Applications: concepts/template_applications.md # TODO: make tutorial
- Cloud Deploy: cloud/quick_start.md
- Guides:
- How-to Guides:
- how-tos/index.md
- LangGraph:
- LangGraph: how-tos#langgraph
- Controllability:
- Controllability: how-tos#controllability
- how-tos/branching.ipynb
- how-tos/map-reduce.ipynb
- how-tos/recursion-limit.ipynb
- how-tos/command.ipynb
- Persistence:
- Persistence: how-tos#persistence
- how-tos/persistence.ipynb
- how-tos/subgraph-persistence.ipynb
- how-tos/cross-thread-persistence.ipynb
- how-tos/persistence_postgres.ipynb
- how-tos/persistence_mongodb.ipynb
- how-tos/persistence_redis.ipynb
- Memory:
- Memory: how-tos#memory
- how-tos/memory/manage-conversation-history.ipynb
- how-tos/memory/delete-messages.ipynb
- how-tos/memory/add-summary-conversation-history.ipynb
- how-tos/memory/semantic-search.ipynb
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop_1
- cloud/how-tos/human_in_the_loop_breakpoint.md
- cloud/how-tos/human_in_the_loop_user_input.md
- cloud/how-tos/human_in_the_loop_edit_state.md
- cloud/how-tos/human_in_the_loop_time_travel.md
- cloud/how-tos/human_in_the_loop_review_tool_calls.md
- Double-texting:
- Double-texting: how-tos#double-texting
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Webhooks:
- cloud/how-tos/webhooks.md
- Cron Jobs:
- cloud/how-tos/cron_jobs.md
- LangGraph Studio:
- LangGraph Studio: how-tos#langgraph-studio
- cloud/how-tos/test_deployment.md
- 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
- Human-in-the-loop: how-tos#human-in-the-loop
- how-tos/human_in_the_loop/breakpoints.ipynb
- how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
- how-tos/human_in_the_loop/edit-graph-state.ipynb
- how-tos/human_in_the_loop/wait-user-input.ipynb
- how-tos/human_in_the_loop/time-travel.ipynb
- how-tos/human_in_the_loop/review-tool-calls.ipynb
- Streaming:
- Streaming: how-tos#streaming
- how-tos/stream-values.ipynb
- how-tos/stream-updates.ipynb
- how-tos/streaming-tokens.ipynb
- how-tos/streaming-tokens-without-langchain.ipynb
- how-tos/streaming-content.ipynb
- how-tos/stream-multiple.ipynb
- how-tos/streaming-events-from-within-tools.ipynb
- how-tos/streaming-events-from-within-tools-without-langchain.ipynb
- how-tos/streaming-from-final-node.ipynb
- how-tos/streaming-subgraphs.ipynb
- how-tos/disable-streaming.ipynb
- Tool calling:
- Tool calling: how-tos#tool-calling
- 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:
- Subgraphs: how-tos#subgraphs
- how-tos/subgraph.ipynb
- how-tos/subgraphs-manage-state.ipynb
- how-tos/subgraph-transform-state.ipynb
- Multi-agent:
- Multi-agent: how-tos#multi-agent
- how-tos/agent-handoffs.ipynb
- how-tos/multi-agent-network.ipynb
- how-tos/multi-agent-multi-turn-convo.ipynb
- State Management:
- State Management: how-tos#state-management
- how-tos/state-model.ipynb
- how-tos/input_output_schema.ipynb
- how-tos/pass_private_state.ipynb
- Other:
- Other: how-tos#other
- how-tos/async.ipynb
- how-tos/visualization.ipynb
- how-tos/configuration.ipynb
- how-tos/node-retries.ipynb
- how-tos/react-agent-structured-output.ipynb
- how-tos/run-id-langsmith.ipynb
- how-tos/return-when-recursion-limit-hits.ipynb
- Prebuilt ReAct Agent:
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
- how-tos/create-react-agent.ipynb
- how-tos/create-react-agent-memory.ipynb
- how-tos/create-react-agent-system-prompt.ipynb
- how-tos/create-react-agent-hitl.ipynb
- how-tos/create-react-agent-structured-output.ipynb
- how-tos/react-agent-from-scratch.ipynb
- LangGraph Platform:
- LangGraph Platform: how-tos#langgraph-platform
- Application Structure:
- Application Structure: how-tos#application-structure
- cloud/deployment/setup.md
- cloud/deployment/setup_pyproject.md
- cloud/deployment/setup_javascript.md
- cloud/deployment/semantic_search.md
- cloud/deployment/custom_docker.md
- cloud/deployment/test_locally.md
- cloud/deployment/graph_rebuild.md
- Deployment:
- Deployment: how-tos#deployment
- cloud/deployment/cloud.md
- how-tos/deploy-self-hosted.md
- how-tos/use-remote-graph.md
- Authentication & Access Control:
- Authentication & Access Control: how-tos#authentication-access-control
- cloud/how-tos/auth/custom_auth_new.md
- cloud/how-tos/auth/openapi_security_new.md
- Assistants:
- Assistants: how-tos#assistants
- cloud/how-tos/configuration_cloud.md
- cloud/how-tos/assistant_versioning.md
- Threads:
- Threads: how-tos#threads
- cloud/how-tos/copy_threads.md
- cloud/how-tos/check_thread_status.md
- Runs:
- Runs: how-tos#runs
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- Streaming:
- Streaming: how-tos#streaming_1
- cloud/how-tos/stream_values.md
- cloud/how-tos/stream_updates.md
- cloud/how-tos/stream_messages.md
- cloud/how-tos/stream_events.md
- cloud/how-tos/stream_debug.md
- cloud/how-tos/stream_multiple.md
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop_1
- cloud/how-tos/human_in_the_loop_breakpoint.md
- cloud/how-tos/human_in_the_loop_user_input.md
- cloud/how-tos/human_in_the_loop_edit_state.md
- cloud/how-tos/human_in_the_loop_time_travel.md
- cloud/how-tos/human_in_the_loop_review_tool_calls.md
- Double-texting:
- Double-texting: how-tos#double-texting
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Webhooks:
- cloud/how-tos/webhooks.md
- Cron Jobs:
- cloud/how-tos/cron_jobs.md
- LangGraph Studio:
- LangGraph Studio: how-tos#langgraph-studio
- cloud/how-tos/test_deployment.md
- 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
- Concepts:
- concepts/index.md
- LangGraph:
- LangGraph: concepts#langgraph
- concepts/high_level.md
- concepts/low_level.md
- concepts/agentic_concepts.md
- concepts/multi_agent.md
- concepts/breakpoints
- concepts/human_in_the_loop.md
- concepts/time-travel.md
- concepts/persistence.md
- concepts/memory.md
- concepts/streaming.md
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- High Level:
- High Level: concepts#high-level
- concepts/langgraph_platform.md
- concepts/deployment_options.md
- concepts/plans.md
- concepts/template_applications.md
- Components:
- Components: concepts#components
- concepts/langgraph_server.md
- concepts/langgraph_studio.md
- concepts/langgraph_cli.md
- concepts/sdk.md
- how-tos/use-remote-graph.md
- LangGraph Server:
- LangGraph Server: concepts#langgraph-server
- concepts/application_structure.md
- concepts/assistants.md
- concepts/double_texting.md
- concepts/auth.md
- Deployment Options:
- Deployment Options: concepts#deployment-options
- concepts/self_hosted.md
- concepts/langgraph_cloud.md
- concepts/bring_your_own_cloud.md
- Tutorials:
- tutorials/index.md
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots:
- Chatbots: tutorials#chatbots
- tutorials/customer-support/customer-support.ipynb
- tutorials/chatbots/information-gather-prompting.ipynb
- tutorials/code_assistant/langgraph_code_assistant.ipynb
- RAG:
- RAG: tutorials#rag
- tutorials/rag/langgraph_adaptive_rag.ipynb
- tutorials/rag/langgraph_adaptive_rag_local.ipynb
- tutorials/rag/langgraph_agentic_rag.ipynb
- tutorials/rag/langgraph_crag.ipynb
- tutorials/rag/langgraph_crag_local.ipynb
- tutorials/rag/langgraph_self_rag.ipynb
- tutorials/rag/langgraph_self_rag_local.ipynb
- tutorials/sql-agent.ipynb
- Agent Architectures:
- Agent Architectures: tutorials#agent-architectures
- Multi-Agent Systems:
- Multi-Agent Systems: tutorials#multi-agent-systems
- tutorials/multi_agent/multi-agent-collaboration.ipynb
- tutorials/multi_agent/agent_supervisor.ipynb
- tutorials/multi_agent/hierarchical_agent_teams.ipynb
- Planning Agents:
- Planning Agents: tutorials#planning-agents
- tutorials/plan-and-execute/plan-and-execute.ipynb
- tutorials/rewoo/rewoo.ipynb
- tutorials/llm-compiler/LLMCompiler.ipynb
- Reflection & Critique:
- Reflection & Critique: tutorials#reflection-critique
- tutorials/reflection/reflection.ipynb
- tutorials/reflexion/reflexion.ipynb
- tutorials/tot/tot.ipynb
- tutorials/lats/lats.ipynb
- tutorials/self-discover/self-discover.ipynb
- Evaluation & Analysis:
- Evaluation & Analysis: tutorials#evaluation
- tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
- tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
- Experimental:
- Experimental: tutorials#experimental
- tutorials/storm/storm.ipynb
- tutorials/tnt-llm/tnt-llm.ipynb
- tutorials/web-navigation/web_voyager.ipynb
- tutorials/usaco/usaco.ipynb
- tutorials/extraction/retries.ipynb
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- tutorials/auth/getting_started.md
- tutorials/auth/resource_auth.md
- tutorials/auth/add_auth_server.md
- Resources:
- FAQ: concepts/faq.md
- Troubleshooting:
- Troubleshooting: how-tos#troubleshooting
- Troubleshooting: troubleshooting/errors/index.md
- troubleshooting/errors/index.md
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
- Conceptual Guides:
- concepts/index.md
- LangGraph:
- LangGraph: concepts#langgraph
- concepts/high_level.md
- concepts/low_level.md
- concepts/agentic_concepts.md
- concepts/multi_agent.md
- concepts/human_in_the_loop.md
- concepts/persistence.md
- concepts/memory.md
- concepts/streaming.md
- concepts/faq.md
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- High Level:
- High Level: concepts#high-level
- concepts/langgraph_platform.md
- concepts/deployment_options.md
- concepts/plans.md
- concepts/template_applications.md
- Components:
- Components: concepts#components
- concepts/langgraph_server.md
- concepts/langgraph_studio.md
- concepts/langgraph_cli.md
- concepts/sdk.md
- how-tos/use-remote-graph.md
- LangGraph Server:
- LangGraph Server: concepts#langgraph-server
- concepts/application_structure.md
- concepts/assistants.md
- concepts/double_texting.md
- Deployment Options:
- Deployment Options: concepts#deployment-options
- concepts/self_hosted.md
- concepts/langgraph_cloud.md
- concepts/bring_your_own_cloud.md
- Reference:
- "reference/index.md"
- Library:
- Graphs: reference/graphs.md
- Checkpointing: reference/checkpoints.md
- Storage: reference/store.md
- Prebuilt Components: reference/prebuilt.md
- Channels: reference/channels.md
- Errors: reference/errors.md
- Types: reference/types.md
- Constants: reference/constants.md
- LangGraph Platform:
- Server API: "cloud/reference/api/api_ref.md"
- CLI: "cloud/reference/cli.md"
- SDK (Python): "cloud/reference/sdk/python_sdk_ref.md"
- SDK (JS/TS): "cloud/reference/sdk/js_ts_sdk_ref.md"
- RemoteGraph: reference/remote_graph.md
- Environment Variables: "cloud/reference/env_var.md"
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
- LangGraph Academy Course: https://academy.langchain.com/courses/intro-to-langgraph
- API reference:
- Library:
- Graphs: reference/graphs.md
- Checkpointing: reference/checkpoints.md
- Storage: reference/store.md
- Prebuilt components: reference/prebuilt.md
- Channels: reference/channels.md
- Errors: reference/errors.md
- Types: reference/types.md
- Constants: reference/constants.md
- Functional API: reference/func.md
- LangGraph Platform:
- Server API: "cloud/reference/api/api_ref.md"
- CLI: "cloud/reference/cli.md"
- SDK (Python): "cloud/reference/sdk/python_sdk_ref.md"
- SDK (JS/TS): "cloud/reference/sdk/js_ts_sdk_ref.md"
- RemoteGraph: reference/remote_graph.md
- Environment variables: "cloud/reference/env_var.md"
markdown_extensions:
- abbr
+10 -11
View File
@@ -34,17 +34,6 @@
color: #1E88E5;
}
.md-sidebar {
display: none;
}
/* Show sidebar on mobile */
@media screen and (max-width: 1220px) {
.md-sidebar--primary {
display: block;
}
}
.md-typeset a:hover {
color: #1565C0;
}
@@ -169,6 +158,16 @@
background-color: #CFC9FA;
color: #000000;
}
/* control the navbar depth */
[data-md-level="2"] .md-nav {
display: none;
}
/* disable the collapse/expand icon in the navar */
.md-nav__icon {
display: none;
}
</style>
{% endblock %}
-35
View File
@@ -1,35 +0,0 @@
.PHONY: test test_watch lint format
######################
# TESTING AND COVERAGE
######################
test:
poetry run pytest tests
test_watch:
poetry run ptw .
######################
# LINTING AND FORMATTING
######################
# Define a variable for Python and notebook files.
PYTHON_FILES=.
MYPY_CACHE=.mypy_cache
lint format: PYTHON_FILES=.
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
lint_package: PYTHON_FILES=langgraph
lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests:
poetry run ruff check .
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
[ "$(PYTHON_FILES)" = "" ] || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff:
poetry run ruff format $(PYTHON_FILES)
poetry run ruff check --select I --fix $(PYTHON_FILES)
-95
View File
@@ -1,95 +0,0 @@
# LangGraph Checkpoint DuckDB
Implementation of LangGraph CheckpointSaver that uses DuckDB.
## Usage
> [!IMPORTANT]
> When using DuckDB checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
```python
from langgraph.checkpoint.duckdb import DuckDBSaver
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
with DuckDBSaver.from_conn_string(":memory:") as checkpointer:
# call .setup() the first time you're using the checkpointer
checkpointer.setup()
checkpoint = {
"v": 1,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
"pending_sends": [],
}
# store checkpoint
checkpointer.put(write_config, checkpoint, {}, {})
# load checkpoint
checkpointer.get(read_config)
# list checkpoints
list(checkpointer.list(read_config))
```
### Async
```python
from langgraph.checkpoint.duckdb.aio import AsyncDuckDBSaver
async with AsyncDuckDBSaver.from_conn_string(":memory:") as checkpointer:
checkpoint = {
"v": 1,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
"pending_sends": [],
}
# store checkpoint
await checkpointer.aput(write_config, checkpoint, {}, {})
# load checkpoint
await checkpointer.aget(read_config)
# list checkpoints
[c async for c in checkpointer.alist(read_config)]
```
@@ -1,356 +0,0 @@
import threading
from contextlib import contextmanager
from typing import Any, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
import duckdb
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
)
from langgraph.checkpoint.duckdb.base import BaseDuckDBSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
class DuckDBSaver(BaseDuckDBSaver):
lock: threading.Lock
def __init__(
self,
conn: duckdb.DuckDBPyConnection,
serde: Optional[SerializerProtocol] = None,
) -> None:
super().__init__(serde=serde)
self.conn = conn
self.lock = threading.Lock()
@classmethod
@contextmanager
def from_conn_string(cls, conn_string: str) -> Iterator["DuckDBSaver"]:
"""Create a new DuckDBSaver instance from a connection string.
Args:
conn_string (str): The DuckDB connection info string.
Returns:
DuckDBSaver: A new DuckDBSaver instance.
"""
with duckdb.connect(conn_string) as conn:
yield cls(conn)
def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
This method creates the necessary tables in the DuckDB database if they don't
already exist and runs database migrations. It MUST be called directly by the user
the first time checkpointer is used.
"""
with self.lock, self.conn.cursor() as cur:
try:
row = cur.execute(
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
).fetchone()
if row is None:
version = -1
else:
version = row[0]
except duckdb.CatalogException:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
cur.execute(migration)
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (?)", [v])
def list(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
This method retrieves a list of checkpoint tuples from the DuckDB database based
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
Args:
config (RunnableConfig): The config to use for listing the checkpoints.
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None.
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None.
Yields:
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
Examples:
>>> from langgraph.checkpoint.duckdb import DuckDBSaver
>>> with DuckDBSaver.from_conn_string(":memory:") as memory:
... # Run a graph, then list the checkpoints
>>> config = {"configurable": {"thread_id": "1"}}
>>> checkpoints = list(memory.list(config, limit=2))
>>> print(checkpoints)
[CheckpointTuple(...), CheckpointTuple(...)]
>>> config = {"configurable": {"thread_id": "1"}}
>>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
>>> with DuckDBSaver.from_conn_string(":memory:") as memory:
... # Run a graph, then list the checkpoints
>>> checkpoints = list(memory.list(config, before=before))
>>> print(checkpoints)
[CheckpointTuple(...), ...]
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
if limit:
query += f" LIMIT {limit}"
# if we change this to use .stream() we need to make sure to close the cursor
with self._cursor() as cur:
cur.execute(query, args)
for value in cur.fetchall():
(
thread_id,
checkpoint,
checkpoint_ns,
checkpoint_id,
parent_checkpoint_id,
metadata,
channel_values,
pending_writes,
pending_sends,
) = value
yield CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
}
},
self._load_checkpoint(
checkpoint,
channel_values,
pending_sends,
),
self._load_metadata(metadata),
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
if parent_checkpoint_id
else None
),
self._load_writes(pending_writes),
)
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the DuckDB database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
Args:
config (RunnableConfig): The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
Examples:
Basic:
>>> config = {"configurable": {"thread_id": "1"}}
>>> checkpoint_tuple = memory.get_tuple(config)
>>> print(checkpoint_tuple)
CheckpointTuple(...)
With timestamp:
>>> config = {
... "configurable": {
... "thread_id": "1",
... "checkpoint_ns": "",
... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
... }
... }
>>> checkpoint_tuple = memory.get_tuple(config)
>>> print(checkpoint_tuple)
CheckpointTuple(...)
""" # noqa
thread_id = config["configurable"]["thread_id"]
checkpoint_id = get_checkpoint_id(config)
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
if checkpoint_id:
args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
where = "WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?"
else:
args = (thread_id, checkpoint_ns)
where = "WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY checkpoint_id DESC LIMIT 1"
with self._cursor() as cur:
cur.execute(
self.SELECT_SQL + where,
args,
)
value = cur.fetchone()
if value:
(
thread_id,
checkpoint,
checkpoint_ns,
checkpoint_id,
parent_checkpoint_id,
metadata,
channel_values,
pending_writes,
pending_sends,
) = value
return CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
}
},
self._load_checkpoint(
checkpoint,
channel_values,
pending_sends,
),
self._load_metadata(metadata),
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
if parent_checkpoint_id
else None
),
self._load_writes(pending_writes),
)
def put(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save a checkpoint to the database.
This method saves a checkpoint to the DuckDB database. The checkpoint is associated
with the provided config and its parent config (if any).
Args:
config (RunnableConfig): The config to associate with the checkpoint.
checkpoint (Checkpoint): The checkpoint to save.
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
new_versions (ChannelVersions): New channel versions as of this write.
Returns:
RunnableConfig: Updated configuration after storing the checkpoint.
Examples:
>>> from langgraph.checkpoint.duckdb import DuckDBSaver
>>> with DuckDBSaver.from_conn_string(":memory:") as memory:
>>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
>>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
>>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
>>> print(saved_config)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
"""
configurable = config["configurable"].copy()
thread_id = configurable.pop("thread_id")
checkpoint_ns = configurable.pop("checkpoint_ns")
checkpoint_id = configurable.pop(
"checkpoint_id", configurable.pop("thread_ts", None)
)
copy = checkpoint.copy()
next_config = {
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint["id"],
}
}
checkpoint_blobs = self._dump_blobs(
thread_id,
checkpoint_ns,
copy.pop("channel_values"), # type: ignore[misc]
new_versions,
)
with self._cursor() as cur:
if checkpoint_blobs:
cur.executemany(self.UPSERT_CHECKPOINT_BLOBS_SQL, checkpoint_blobs)
cur.execute(
self.UPSERT_CHECKPOINTS_SQL,
(
thread_id,
checkpoint_ns,
checkpoint["id"],
checkpoint_id,
self._dump_checkpoint(copy),
self._dump_metadata(metadata),
),
)
return next_config
def put_writes(
self,
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
) -> None:
"""Store intermediate writes linked to a checkpoint.
This method saves intermediate writes associated with a checkpoint to the DuckDB database.
Args:
config (RunnableConfig): Configuration of the related checkpoint.
writes (List[Tuple[str, Any]]): List of writes to store.
task_id (str): Identifier for the task creating the writes.
"""
query = (
self.UPSERT_CHECKPOINT_WRITES_SQL
if all(w[0] in WRITES_IDX_MAP for w in writes)
else self.INSERT_CHECKPOINT_WRITES_SQL
)
with self._cursor() as cur:
cur.executemany(
query,
self._dump_writes(
config["configurable"]["thread_id"],
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
writes,
),
)
@contextmanager
def _cursor(self) -> Iterator[duckdb.DuckDBPyConnection]:
with self.lock, self.conn.cursor() as cur:
yield cur
__all__ = ["DuckDBSaver", "Conn"]
@@ -1,443 +0,0 @@
import asyncio
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
import duckdb
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
)
from langgraph.checkpoint.duckdb.base import BaseDuckDBSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
class AsyncDuckDBSaver(BaseDuckDBSaver):
lock: asyncio.Lock
def __init__(
self,
conn: duckdb.DuckDBPyConnection,
serde: Optional[SerializerProtocol] = None,
) -> None:
super().__init__(serde=serde)
self.conn = conn
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
@classmethod
@asynccontextmanager
async def from_conn_string(
cls,
conn_string: str,
) -> AsyncIterator["AsyncDuckDBSaver"]:
"""Create a new AsyncDuckDBSaver instance from a connection string.
Args:
conn_string (str): The DuckDB connection info string.
Returns:
AsyncDuckDBSaver: A new AsyncDuckDBSaver instance.
"""
with duckdb.connect(conn_string) as conn:
yield cls(conn)
async def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
This method creates the necessary tables in the DuckDB database if they don't
already exist and runs database migrations. It MUST be called directly by the user
the first time checkpointer is used.
"""
async with self.lock:
with self.conn.cursor() as cur:
try:
await asyncio.to_thread(
cur.execute,
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1",
)
row = await asyncio.to_thread(cur.fetchone)
if row is None:
version = -1
else:
version = row[0]
except duckdb.CatalogException:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
await asyncio.to_thread(cur.execute, migration)
await asyncio.to_thread(
cur.execute,
"INSERT INTO checkpoint_migrations (v) VALUES (?)",
[v],
)
async def alist(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> AsyncIterator[CheckpointTuple]:
"""List checkpoints from the database asynchronously.
This method retrieves a list of checkpoint tuples from the DuckDB database based
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
Args:
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit (Optional[int]): Maximum number of checkpoints to return.
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
if limit:
query += f" LIMIT {limit}"
# if we change this to use .stream() we need to make sure to close the cursor
async with self._cursor() as cur:
await asyncio.to_thread(cur.execute, query, args)
results = await asyncio.to_thread(cur.fetchall)
for value in results:
(
thread_id,
checkpoint,
checkpoint_ns,
checkpoint_id,
parent_checkpoint_id,
metadata,
channel_values,
pending_writes,
pending_sends,
) = value
yield CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
}
},
await asyncio.to_thread(
self._load_checkpoint,
checkpoint,
channel_values,
pending_sends,
),
self._load_metadata(metadata),
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
if parent_checkpoint_id
else None
),
await asyncio.to_thread(self._load_writes, pending_writes),
)
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database asynchronously.
This method retrieves a checkpoint tuple from the DuckDBdatabase based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
Args:
config (RunnableConfig): The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
thread_id = config["configurable"]["thread_id"]
checkpoint_id = get_checkpoint_id(config)
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
if checkpoint_id:
args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
where = "WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?"
else:
args = (thread_id, checkpoint_ns)
where = "WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY checkpoint_id DESC LIMIT 1"
async with self._cursor() as cur:
await asyncio.to_thread(
cur.execute,
self.SELECT_SQL + where,
args,
)
value = await asyncio.to_thread(cur.fetchone)
if value:
(
thread_id,
checkpoint,
checkpoint_ns,
checkpoint_id,
parent_checkpoint_id,
metadata,
channel_values,
pending_writes,
pending_sends,
) = value
return CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
}
},
await asyncio.to_thread(
self._load_checkpoint,
checkpoint,
channel_values,
pending_sends,
),
self._load_metadata(metadata),
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
if parent_checkpoint_id
else None
),
await asyncio.to_thread(self._load_writes, pending_writes),
)
async def aput(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save a checkpoint to the database asynchronously.
This method saves a checkpoint to the DuckDB database. The checkpoint is associated
with the provided config and its parent config (if any).
Args:
config (RunnableConfig): The config to associate with the checkpoint.
checkpoint (Checkpoint): The checkpoint to save.
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
new_versions (ChannelVersions): New channel versions as of this write.
Returns:
RunnableConfig: Updated configuration after storing the checkpoint.
"""
configurable = config["configurable"].copy()
thread_id = configurable.pop("thread_id")
checkpoint_ns = configurable.pop("checkpoint_ns")
checkpoint_id = configurable.pop(
"checkpoint_id", configurable.pop("thread_ts", None)
)
copy = checkpoint.copy()
next_config = {
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint["id"],
}
}
checkpoint_blobs = await asyncio.to_thread(
self._dump_blobs,
thread_id,
checkpoint_ns,
copy.pop("channel_values"), # type: ignore[misc]
new_versions,
)
async with self._cursor() as cur:
if checkpoint_blobs:
await asyncio.to_thread(
cur.executemany, self.UPSERT_CHECKPOINT_BLOBS_SQL, checkpoint_blobs
)
await asyncio.to_thread(
cur.execute,
self.UPSERT_CHECKPOINTS_SQL,
(
thread_id,
checkpoint_ns,
checkpoint["id"],
checkpoint_id,
self._dump_checkpoint(copy),
self._dump_metadata(metadata),
),
)
return next_config
async def aput_writes(
self,
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
) -> None:
"""Store intermediate writes linked to a checkpoint asynchronously.
This method saves intermediate writes associated with a checkpoint to the database.
Args:
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
"""
query = (
self.UPSERT_CHECKPOINT_WRITES_SQL
if all(w[0] in WRITES_IDX_MAP for w in writes)
else self.INSERT_CHECKPOINT_WRITES_SQL
)
params = await asyncio.to_thread(
self._dump_writes,
config["configurable"]["thread_id"],
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
writes,
)
async with self._cursor() as cur:
await asyncio.to_thread(cur.executemany, query, params)
@asynccontextmanager
async def _cursor(self) -> AsyncIterator[duckdb.DuckDBPyConnection]:
async with self.lock:
with self.conn.cursor() as cur:
yield cur
def list(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
This method retrieves a list of checkpoint tuples from the DuckDB database based
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
Args:
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit (Optional[int]): Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
"""
try:
# check if we are in the main thread, only bg threads can block
# we don't check in other methods to avoid the overhead
if asyncio.get_running_loop() is self.loop:
raise asyncio.InvalidStateError(
"Synchronous calls to AsyncDuckDBSaver are only allowed from a "
"different thread. From the main thread, use the async interface. "
"For example, use `checkpointer.alist(...)` or `await "
"graph.ainvoke(...)`."
)
except RuntimeError:
pass
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_),
self.loop,
).result()
except StopAsyncIteration:
break
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the DuckDB database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
Args:
config (RunnableConfig): The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
try:
# check if we are in the main thread, only bg threads can block
# we don't check in other methods to avoid the overhead
if asyncio.get_running_loop() is self.loop:
raise asyncio.InvalidStateError(
"Synchronous calls to AsyncDuckDBSaver are only allowed from a "
"different thread. From the main thread, use the async interface."
"For example, use `await checkpointer.aget_tuple(...)` or `await "
"graph.ainvoke(...)`."
)
except RuntimeError:
pass
return asyncio.run_coroutine_threadsafe(
self.aget_tuple(config), self.loop
).result()
def put(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save a checkpoint to the database.
This method saves a checkpoint to the DuckDB database. The checkpoint is associated
with the provided config and its parent config (if any).
Args:
config (RunnableConfig): The config to associate with the checkpoint.
checkpoint (Checkpoint): The checkpoint to save.
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
new_versions (ChannelVersions): New channel versions as of this write.
Returns:
RunnableConfig: Updated configuration after storing the checkpoint.
"""
return asyncio.run_coroutine_threadsafe(
self.aput(config, checkpoint, metadata, new_versions), self.loop
).result()
def put_writes(
self,
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
) -> None:
"""Store intermediate writes linked to a checkpoint.
This method saves intermediate writes associated with a checkpoint to the database.
Args:
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
"""
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
).result()
@@ -1,290 +0,0 @@
import json
import random
from typing import Any, List, Optional, Sequence, Tuple, cast
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
get_checkpoint_id,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
MetadataInput = Optional[dict[str, Any]]
"""
To add a new migration, add a new string to the MIGRATIONS list.
The position of the migration in the list is the version number.
"""
MIGRATIONS = [
"""CREATE TABLE IF NOT EXISTS checkpoint_migrations (
v INTEGER PRIMARY KEY
);""",
"""CREATE TABLE IF NOT EXISTS checkpoints (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
parent_checkpoint_id TEXT,
type TEXT,
checkpoint JSON NOT NULL,
metadata JSON NOT NULL DEFAULT '{}',
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
);""",
"""CREATE TABLE IF NOT EXISTS checkpoint_blobs (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
channel TEXT NOT NULL,
version TEXT NOT NULL,
type TEXT NOT NULL,
blob BLOB,
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
);""",
"""CREATE TABLE IF NOT EXISTS checkpoint_writes (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
task_id TEXT NOT NULL,
idx INTEGER NOT NULL,
channel TEXT NOT NULL,
type TEXT,
blob BLOB NOT NULL,
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
);""",
]
SELECT_SQL = f"""
select
thread_id,
checkpoint,
checkpoint_ns,
checkpoint_id,
parent_checkpoint_id,
metadata,
(
select array_agg(array[bl.channel::bytea, bl.type::bytea, bl.blob])
from (
SELECT unnest(json_keys(json_extract(checkpoint, '$.channel_versions'))) as key
) cv
inner join checkpoint_blobs bl
on bl.thread_id = checkpoints.thread_id
and bl.checkpoint_ns = checkpoints.checkpoint_ns
and bl.channel = cv.key
and bl.version = json_extract_string(checkpoint, '$.channel_versions.' || cv.key)
) as channel_values,
(
select
array_agg(array[cw.task_id::blob, cw.channel::blob, cw.type::blob, cw.blob])
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
and cw.checkpoint_id = checkpoints.checkpoint_id
) as pending_writes,
(
select array_agg(array[cw.type::blob, cw.blob])
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
and cw.checkpoint_id = checkpoints.parent_checkpoint_id
and cw.channel = '{TASKS}'
) as pending_sends
from checkpoints """
UPSERT_CHECKPOINT_BLOBS_SQL = """
INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (thread_id, checkpoint_ns, channel, version) DO NOTHING
"""
UPSERT_CHECKPOINTS_SQL = """
INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id)
DO UPDATE SET
checkpoint = EXCLUDED.checkpoint,
metadata = EXCLUDED.metadata;
"""
UPSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
channel = EXCLUDED.channel,
type = EXCLUDED.type,
blob = EXCLUDED.blob;
"""
INSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
"""
class BaseDuckDBSaver(BaseCheckpointSaver[str]):
SELECT_SQL = SELECT_SQL
MIGRATIONS = MIGRATIONS
UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
jsonplus_serde = JsonPlusSerializer()
def _load_checkpoint(
self,
checkpoint_json_str: str,
channel_values: list[tuple[bytes, bytes, bytes]],
pending_sends: list[tuple[bytes, bytes]],
) -> Checkpoint:
checkpoint = json.loads(checkpoint_json_str)
return {
**checkpoint,
"pending_sends": [
self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends or []
],
"channel_values": self._load_blobs(channel_values),
}
def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]:
return {**checkpoint, "pending_sends": []}
def _load_blobs(
self, blob_values: list[tuple[bytes, bytes, bytes]]
) -> dict[str, Any]:
if not blob_values:
return {}
return {
k.decode(): self.serde.loads_typed((t.decode(), v))
for k, t, v in blob_values
if t.decode() != "empty"
}
def _dump_blobs(
self,
thread_id: str,
checkpoint_ns: str,
values: dict[str, Any],
versions: ChannelVersions,
) -> list[tuple[str, str, str, str, str, Optional[bytes]]]:
if not versions:
return []
return [
(
thread_id,
checkpoint_ns,
k,
cast(str, ver),
*(
self.serde.dumps_typed(values[k])
if k in values
else ("empty", None)
),
)
for k, ver in versions.items()
]
def _load_writes(
self, writes: list[tuple[bytes, bytes, bytes, bytes]]
) -> list[tuple[str, str, Any]]:
return (
[
(
tid.decode(),
channel.decode(),
self.serde.loads_typed((t.decode(), v)),
)
for tid, channel, t, v in writes
]
if writes
else []
)
def _dump_writes(
self,
thread_id: str,
checkpoint_ns: str,
checkpoint_id: str,
task_id: str,
writes: Sequence[tuple[str, Any]],
) -> list[tuple[str, str, str, str, int, str, str, bytes]]:
return [
(
thread_id,
checkpoint_ns,
checkpoint_id,
task_id,
WRITES_IDX_MAP.get(channel, idx),
channel,
*self.serde.dumps_typed(value),
)
for idx, (channel, value) in enumerate(writes)
]
def _load_metadata(self, metadata_json_str: str) -> CheckpointMetadata:
return self.jsonplus_serde.loads(metadata_json_str.encode())
def _dump_metadata(self, metadata: CheckpointMetadata) -> str:
serialized_metadata = self.jsonplus_serde.dumps(metadata)
# NOTE: we're using JSON serializer (not msgpack), so we need to remove null characters before writing
return serialized_metadata.decode().replace("\\u0000", "")
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
if current is None:
current_v = 0
elif isinstance(current, int):
current_v = current
else:
current_v = int(current.split(".")[0])
next_v = current_v + 1
next_h = random.random()
return f"{next_v:032}.{next_h:016}"
def _search_where(
self,
config: Optional[RunnableConfig],
filter: MetadataInput,
before: Optional[RunnableConfig] = None,
) -> Tuple[str, List[Any]]:
"""Return WHERE clause predicates for alist() given config, filter, before.
This method returns a tuple of a string and a tuple of values. The string
is the parametered WHERE clause predicate (including the WHERE keyword):
"WHERE column1 = $1 AND column2 IS $2". The list of values contains the
values for each of the corresponding parameters.
"""
wheres = []
param_values = []
# construct predicate for config filter
if config:
wheres.append("thread_id = ?")
param_values.append(config["configurable"]["thread_id"])
checkpoint_ns = config["configurable"].get("checkpoint_ns")
if checkpoint_ns is not None:
wheres.append("checkpoint_ns = ?")
param_values.append(checkpoint_ns)
if checkpoint_id := get_checkpoint_id(config):
wheres.append("checkpoint_id = ?")
param_values.append(checkpoint_id)
# construct predicate for metadata filter
if filter:
wheres.append("json_contains(metadata, ?)")
param_values.append(json.dumps(filter))
# construct predicate for `before`
if before is not None:
wheres.append("checkpoint_id < ?")
param_values.append(get_checkpoint_id(before))
return (
"WHERE " + " AND ".join(wheres) if wheres else "",
param_values,
)
@@ -1,4 +0,0 @@
from langgraph.store.duckdb.aio import AsyncDuckDBStore
from langgraph.store.duckdb.base import DuckDBStore
__all__ = ["AsyncDuckDBStore", "DuckDBStore"]
@@ -1,195 +0,0 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import (
AsyncIterator,
Iterable,
Sequence,
cast,
)
import duckdb
from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, SearchOp
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.duckdb.base import (
BaseDuckDBStore,
_convert_ns,
_group_ops,
_row_to_item,
)
logger = logging.getLogger(__name__)
class AsyncDuckDBStore(AsyncBatchedBaseStore, BaseDuckDBStore):
def __init__(
self,
conn: duckdb.DuckDBPyConnection,
) -> None:
super().__init__()
self.conn = conn
self.loop = asyncio.get_running_loop()
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
tasks = []
if GetOp in grouped_ops:
tasks.append(
self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
)
)
if PutOp in grouped_ops:
tasks.append(
self._batch_put_ops(
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp])
)
)
if SearchOp in grouped_ops:
tasks.append(
self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
)
)
if ListNamespacesOp in grouped_ops:
tasks.append(
self._batch_list_namespaces_ops(
cast(
Sequence[tuple[int, ListNamespacesOp]],
grouped_ops[ListNamespacesOp],
),
results,
)
)
await asyncio.gather(*tasks)
return results
def batch(self, ops: Iterable[Op]) -> list[Result]:
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
async def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
) -> None:
cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
cur = self.conn.cursor()
await asyncio.to_thread(cur.execute, query, params)
cursors.append((cur, namespace, items))
for cur, namespace, items in cursors:
rows = await asyncio.to_thread(cur.fetchall)
key_to_row = {row[1]: row for row in rows}
for idx, key in items:
row = key_to_row.get(key)
if row:
results[idx] = _row_to_item(namespace, row)
else:
results[idx] = None
async def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
cur = self.conn.cursor()
await asyncio.to_thread(cur.execute, query, params)
async def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[duckdb.DuckDBPyConnection, int]] = []
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor()
await asyncio.to_thread(cur.execute, query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = await asyncio.to_thread(cur.fetchall)
items = [_row_to_item(_convert_ns(row[0]), row) for row in rows]
results[idx] = items
async def _batch_list_namespaces_ops(
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
cursors: list[tuple[duckdb.DuckDBPyConnection, int]] = []
for (query, params), (idx, _) in zip(queries, list_ops):
cur = self.conn.cursor()
await asyncio.to_thread(cur.execute, query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cast(list[tuple], await asyncio.to_thread(cur.fetchall))
namespaces = [_convert_ns(row[0]) for row in rows]
results[idx] = namespaces
@classmethod
@asynccontextmanager
async def from_conn_string(
cls,
conn_string: str,
) -> AsyncIterator["AsyncDuckDBStore"]:
"""Create a new AsyncDuckDBStore instance from a connection string.
Args:
conn_string (str): The DuckDB connection info string.
Returns:
AsyncDuckDBStore: A new AsyncDuckDBStore instance.
"""
with duckdb.connect(conn_string) as conn:
yield cls(conn)
async def setup(self) -> None:
"""Set up the store database asynchronously.
This method creates the necessary tables in the DuckDB database if they don't
already exist and runs database migrations. It is called automatically when needed and should not be called
directly by the user.
"""
cur = self.conn.cursor()
try:
await asyncio.to_thread(
cur.execute, "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
)
row = await asyncio.to_thread(cur.fetchone)
if row is None:
version = -1
else:
version = row[0]
except duckdb.CatalogException:
version = -1
# Create store_migrations table if it doesn't exist
await asyncio.to_thread(
cur.execute,
"""
CREATE TABLE IF NOT EXISTS store_migrations (
v INTEGER PRIMARY KEY
)
""",
)
for v, migration in enumerate(
self.MIGRATIONS[version + 1 :], start=version + 1
):
await asyncio.to_thread(cur.execute, migration)
await asyncio.to_thread(
cur.execute, "INSERT INTO store_migrations (v) VALUES (?)", (v,)
)
@@ -1,408 +0,0 @@
import asyncio
import json
import logging
from collections import defaultdict
from contextlib import contextmanager
from typing import (
Any,
Generic,
Iterable,
Iterator,
Sequence,
TypeVar,
Union,
cast,
)
import duckdb
from langgraph.store.base import (
BaseStore,
GetOp,
Item,
ListNamespacesOp,
Op,
PutOp,
Result,
SearchItem,
SearchOp,
)
logger = logging.getLogger(__name__)
MIGRATIONS = [
"""
CREATE TABLE IF NOT EXISTS store (
prefix TEXT NOT NULL,
key TEXT NOT NULL,
value JSON NOT NULL,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
PRIMARY KEY (prefix, key)
);
""",
"""
CREATE INDEX IF NOT EXISTS store_prefix_idx ON store (prefix);
""",
]
C = TypeVar("C", bound=duckdb.DuckDBPyConnection)
class BaseDuckDBStore(Generic[C]):
MIGRATIONS = MIGRATIONS
conn: C
def _get_batch_GET_ops_queries(
self,
get_ops: Sequence[tuple[int, GetOp]],
) -> list[tuple[str, tuple, tuple[str, ...], list]]:
namespace_groups = defaultdict(list)
for idx, op in get_ops:
namespace_groups[op.namespace].append((idx, op.key))
results = []
for namespace, items in namespace_groups.items():
_, keys = zip(*items)
keys_to_query = ",".join(["?"] * len(keys))
query = f"""
SELECT prefix, key, value, created_at, updated_at
FROM store
WHERE prefix = ? AND key IN ({keys_to_query})
"""
params = (_namespace_to_text(namespace), *keys)
results.append((query, params, namespace, items))
return results
def _get_batch_PUT_queries(
self,
put_ops: Sequence[tuple[int, PutOp]],
) -> list[tuple[str, Sequence]]:
inserts: list[PutOp] = []
deletes: list[PutOp] = []
for _, op in put_ops:
if op.value is None:
deletes.append(op)
else:
inserts.append(op)
queries: list[tuple[str, Sequence]] = []
if deletes:
namespace_groups: dict[tuple[str, ...], list[str]] = defaultdict(list)
for op in deletes:
namespace_groups[op.namespace].append(op.key)
for namespace, keys in namespace_groups.items():
placeholders = ",".join(["?"] * len(keys))
query = (
f"DELETE FROM store WHERE prefix = ? AND key IN ({placeholders})"
)
params = (_namespace_to_text(namespace), *keys)
queries.append((query, params))
if inserts:
values = []
insertion_params = []
for op in inserts:
values.append("(?, ?, ?, now(), now())")
insertion_params.extend(
[
_namespace_to_text(op.namespace),
op.key,
json.dumps(op.value),
]
)
values_str = ",".join(values)
query = f"""
INSERT INTO store (prefix, key, value, created_at, updated_at)
VALUES {values_str}
ON CONFLICT (prefix, key) DO UPDATE
SET value = EXCLUDED.value, updated_at = now()
"""
queries.append((query, insertion_params))
return queries
def _get_batch_search_queries(
self,
search_ops: Sequence[tuple[int, SearchOp]],
) -> list[tuple[str, Sequence]]:
queries: list[tuple[str, Sequence]] = []
for _, op in search_ops:
query = """
SELECT prefix, key, value, created_at, updated_at
FROM store
WHERE prefix LIKE ?
"""
params: list = [f"{_namespace_to_text(op.namespace_prefix)}%"]
if op.filter:
filter_conditions = []
for key, value in op.filter.items():
filter_conditions.append(f"json_extract(value, '$.{key}') = ?")
params.append(json.dumps(value))
query += " AND " + " AND ".join(filter_conditions)
query += " ORDER BY updated_at DESC LIMIT ? OFFSET ?"
params.extend([op.limit, op.offset])
queries.append((query, params))
return queries
def _get_batch_list_namespaces_queries(
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
) -> list[tuple[str, Sequence]]:
queries: list[tuple[str, Sequence]] = []
for _, op in list_ops:
query = """
WITH split_prefix AS (
SELECT
prefix,
string_split(prefix, '.') AS parts
FROM store
)
SELECT DISTINCT ON (truncated_prefix)
CASE
WHEN ? IS NOT NULL THEN
array_to_string(array_slice(parts, 1, ?), '.')
ELSE prefix
END AS truncated_prefix,
prefix
FROM split_prefix
"""
params: list[Any] = [op.max_depth, op.max_depth]
conditions = []
if op.match_conditions:
for condition in op.match_conditions:
if condition.match_type == "prefix":
conditions.append("prefix LIKE ?")
params.append(
f"{_namespace_to_text(condition.path, handle_wildcards=True)}%"
)
elif condition.match_type == "suffix":
conditions.append("prefix LIKE ?")
params.append(
f"%{_namespace_to_text(condition.path, handle_wildcards=True)}"
)
else:
logger.warning(
f"Unknown match_type in list_namespaces: {condition.match_type}"
)
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY prefix LIMIT ? OFFSET ?"
params.extend([op.limit, op.offset])
queries.append((query, params))
return queries
class DuckDBStore(BaseStore, BaseDuckDBStore[duckdb.DuckDBPyConnection]):
def __init__(
self,
conn: duckdb.DuckDBPyConnection,
) -> None:
super().__init__()
self.conn = conn
def batch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
if GetOp in grouped_ops:
self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
)
if PutOp in grouped_ops:
self._batch_put_ops(cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]))
if SearchOp in grouped_ops:
self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
)
if ListNamespacesOp in grouped_ops:
self._batch_list_namespaces_ops(
cast(
Sequence[tuple[int, ListNamespacesOp]],
grouped_ops[ListNamespacesOp],
),
results,
)
return results
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
return await asyncio.get_running_loop().run_in_executor(None, self.batch, ops)
def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
) -> None:
cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
cur = self.conn.cursor()
cur.execute(query, params)
cursors.append((cur, namespace, items))
for cur, namespace, items in cursors:
rows = cur.fetchall()
key_to_row = {row[1]: row for row in rows}
for idx, key in items:
row = key_to_row.get(key)
if row:
results[idx] = _row_to_item(namespace, row)
else:
results[idx] = None
def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
cur = self.conn.cursor()
cur.execute(query, params)
def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[duckdb.DuckDBPyConnection, int]] = []
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor()
cur.execute(query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cur.fetchall()
items = [_row_to_search_item(_convert_ns(row[0]), row) for row in rows]
results[idx] = items
def _batch_list_namespaces_ops(
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
cursors: list[tuple[duckdb.DuckDBPyConnection, int]] = []
for (query, params), (idx, _) in zip(queries, list_ops):
cur = self.conn.cursor()
cur.execute(query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cast(list[dict], cur.fetchall())
namespaces = [_convert_ns(row[0]) for row in rows]
results[idx] = namespaces
@classmethod
@contextmanager
def from_conn_string(
cls,
conn_string: str,
) -> Iterator["DuckDBStore"]:
"""Create a new BaseDuckDBStore instance from a connection string.
Args:
conn_string (str): The DuckDB connection info string.
Returns:
DuckDBStore: A new DuckDBStore instance.
"""
with duckdb.connect(conn_string) as conn:
yield cls(conn=conn)
def setup(self) -> None:
"""Set up the store database.
This method creates the necessary tables in the DuckDB database if they don't
already exist and runs database migrations. It is called automatically when needed and should not be called
directly by the user.
"""
with self.conn.cursor() as cur:
try:
cur.execute("SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1")
row = cast(dict, cur.fetchone())
if row is None:
version = -1
else:
version = row["v"]
except duckdb.CatalogException:
version = -1
# Create store_migrations table if it doesn't exist
cur.execute(
"""
CREATE TABLE IF NOT EXISTS store_migrations (
v INTEGER PRIMARY KEY
)
"""
)
for v, migration in enumerate(
self.MIGRATIONS[version + 1 :], start=version + 1
):
cur.execute(migration)
cur.execute("INSERT INTO store_migrations (v) VALUES (?)", (v,))
def _namespace_to_text(
namespace: tuple[str, ...], handle_wildcards: bool = False
) -> str:
"""Convert namespace tuple to text string."""
if handle_wildcards:
namespace = tuple("%" if val == "*" else val for val in namespace)
return ".".join(namespace)
def _row_to_item(
namespace: tuple[str, ...],
row: tuple,
) -> Item:
"""Convert a row from the database into an Item."""
_, key, val, created_at, updated_at = row
return Item(
value=val if isinstance(val, dict) else json.loads(val),
key=key,
namespace=namespace,
created_at=created_at,
updated_at=updated_at,
)
def _row_to_search_item(
namespace: tuple[str, ...],
row: tuple,
) -> SearchItem:
"""Convert a row from the database into an SearchItem."""
# TODO: Add support for search
_, key, val, created_at, updated_at = row
return SearchItem(
value=val if isinstance(val, dict) else json.loads(val),
key=key,
namespace=namespace,
created_at=created_at,
updated_at=updated_at,
)
def _group_ops(ops: Iterable[Op]) -> tuple[dict[type, list[tuple[int, Op]]], int]:
grouped_ops: dict[type, list[tuple[int, Op]]] = defaultdict(list)
tot = 0
for idx, op in enumerate(ops):
grouped_ops[type(op)].append((idx, op))
tot += 1
return grouped_ops, tot
def _convert_ns(namespace: Union[str, list]) -> tuple[str, ...]:
if isinstance(namespace, list):
return tuple(namespace)
return tuple(namespace.split("."))
-1058
View File
File diff suppressed because it is too large Load Diff
-60
View File
@@ -1,60 +0,0 @@
[tool.poetry]
name = "langgraph-checkpoint-duckdb"
version = "2.0.2"
description = "Library with a DuckDB implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
readme = "README.md"
repository = "https://www.github.com/langchain-ai/langgraph"
packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.2"
duckdb = ">=1.1.2"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.2"
codespell = "^2.2.0"
pytest = "^7.2.1"
anyio = "^4.4.0"
pytest-asyncio = "^0.21.1"
pytest-mock = "^3.11.1"
pytest-watch = "^4.2.0"
mypy = "^1.10.0"
langgraph-checkpoint = {path = "../checkpoint", develop = true}
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
#
# https://docs.pytest.org/en/7.1.x/reference/reference.html
# --strict-config any warnings encountered while parsing the `pytest`
# section of the configuration file raise errors.
addopts = "--strict-markers --strict-config --durations=5 -vv"
asyncio_mode = "auto"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
lint.select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
]
lint.ignore = ["E501", "B008", "UP007", "UP006"]
[tool.mypy]
# https://mypy.readthedocs.io/en/stable/config_file.html
disallow_untyped_defs = "True"
explicit_package_bases = "True"
warn_no_return = "False"
warn_unused_ignores = "True"
warn_redundant_casts = "True"
allow_redefinition = "True"
disable_error_code = "typeddict-item, return-value"
-112
View File
@@ -1,112 +0,0 @@
from typing import Any
import pytest
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.duckdb.aio import AsyncDuckDBSaver
class TestAsyncDuckDBSaver:
@pytest.fixture(autouse=True)
async def setup(self) -> None:
# objects for test setup
self.config_1: RunnableConfig = {
"configurable": {
"thread_id": "thread-1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_ns": "",
}
}
self.config_2: RunnableConfig = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_id": "2",
"checkpoint_ns": "",
}
}
self.config_3: RunnableConfig = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_id": "2-inner",
"checkpoint_ns": "inner",
}
}
self.chkpnt_1: Checkpoint = empty_checkpoint()
self.chkpnt_2: Checkpoint = create_checkpoint(self.chkpnt_1, {}, 1)
self.chkpnt_3: Checkpoint = empty_checkpoint()
self.metadata_1: CheckpointMetadata = {
"source": "input",
"step": 2,
"writes": {},
"score": 1,
}
self.metadata_2: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
self.metadata_3: CheckpointMetadata = {}
async def test_asearch(self) -> None:
async with AsyncDuckDBSaver.from_conn_string(":memory:") as saver:
await saver.setup()
await saver.aput(self.config_1, self.chkpnt_1, self.metadata_1, {})
await saver.aput(self.config_2, self.chkpnt_2, self.metadata_2, {})
await saver.aput(self.config_3, self.chkpnt_3, self.metadata_3, {})
# call method / assertions
query_1 = {"source": "input"} # search by 1 key
query_2 = {
"step": 1,
"writes": {"foo": "bar"},
} # search by multiple keys
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
query_4 = {"source": "update", "step": 1} # no match
search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
assert len(search_results_1) == 1
assert search_results_1[0].metadata == self.metadata_1
search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
assert len(search_results_2) == 1
assert search_results_2[0].metadata == self.metadata_2
search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
assert len(search_results_3) == 3
search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
assert len(search_results_4) == 0
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = [
c
async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
]
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
async def test_null_chars(self) -> None:
async with AsyncDuckDBSaver.from_conn_string(":memory:") as saver:
await saver.setup()
config = await saver.aput(
self.config_1, self.chkpnt_1, {"my_key": "\x00abc"}, {}
)
assert (await saver.aget_tuple(config)).metadata["my_key"] == "abc" # type: ignore
assert [c async for c in saver.alist(None, filter={"my_key": "abc"})][
0
].metadata["my_key"] == "abc"
@@ -1,517 +0,0 @@
# type: ignore
import uuid
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock
import pytest
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.duckdb import AsyncDuckDBStore
class MockCursor:
def __init__(self, fetch_result: Any) -> None:
self.fetch_result = fetch_result
self.execute = MagicMock()
self.fetchall = MagicMock(return_value=self.fetch_result)
class MockConnection:
def __init__(self) -> None:
self.cursor = MagicMock()
@pytest.fixture
def mock_connection() -> MockConnection:
return MockConnection()
@pytest.fixture
async def store(mock_connection: MockConnection) -> AsyncDuckDBStore:
duck_db_store = AsyncDuckDBStore(mock_connection)
await duck_db_store.setup()
return duck_db_store
async def test_abatch_order(store: AsyncDuckDBStore) -> None:
mock_connection = store.conn
mock_get_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_search_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
]
)
mock_list_namespaces_cursor = MockCursor(
[
("test",),
]
)
failures = []
def cursor_side_effect() -> Any:
cursor = MagicMock()
def execute_side_effect(query: str, *params: Any) -> None:
# My super sophisticated database.
if "WHERE prefix = ? AND key" in query:
cursor.fetchall = mock_get_cursor.fetchall
elif "SELECT prefix, key, value" in query:
cursor.fetchall = mock_search_cursor.fetchall
elif "SELECT DISTINCT ON (truncated_prefix)" in query:
cursor.fetchall = mock_list_namespaces_cursor.fetchall
elif "INSERT INTO " in query:
pass
else:
e = ValueError(f"Unmatched query: {query}")
failures.append(e)
raise e
cursor.execute = MagicMock(side_effect=execute_side_effect)
return cursor
mock_connection.cursor.side_effect = cursor_side_effect # type: ignore
ops = [
GetOp(namespace=("test",), key="key1"),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
results = await store.abatch(ops)
assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
assert results[0].value == {"data": "value1"}
assert results[0].key == "key1"
assert results[1] is None
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
assert results[3] == [("test",)]
assert results[4] is None
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
GetOp(namespace=("test",), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
GetOp(namespace=("test",), key="key1"),
]
results_reordered = await store.abatch(ops_reordered)
assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
assert len(results_reordered[0]) == 1
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
assert results_reordered[2] == [("test",)]
assert results_reordered[3] is None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
assert results_reordered[4].key == "key1"
async def test_batch_get_ops(store: AsyncDuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_connection.cursor.return_value = mock_cursor
ops = [
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test",), key="key3"),
]
results = await store.abatch(ops)
assert len(results) == 3
assert results[0] is not None
assert results[1] is not None
assert results[2] is None
assert results[0].key == "key1"
assert results[1].key == "key2"
async def test_batch_put_ops(store: AsyncDuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor([])
mock_connection.cursor.return_value = mock_cursor
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
PutOp(namespace=("test",), key="key3", value=None),
]
results = await store.abatch(ops)
assert len(results) == 3
assert all(result is None for result in results)
assert mock_cursor.execute.call_count == 2
async def test_batch_search_ops(store: AsyncDuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_connection.cursor.return_value = mock_cursor
ops = [
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
]
results = await store.abatch(ops)
assert len(results) == 2
assert len(results[0]) == 2
assert len(results[1]) == 2
async def test_batch_list_namespaces_ops(store: AsyncDuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor([("test.namespace1",), ("test.namespace2",)])
mock_connection.cursor.return_value = mock_cursor
ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
results = await store.abatch(ops)
assert len(results) == 1
assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
# The following use the actual DB connection
async def test_basic_store_ops() -> None:
async with AsyncDuckDBStore.from_conn_string(":memory:") as store:
await store.setup()
namespace = ("test", "documents")
item_id = "doc1"
item_value = {"title": "Test Document", "content": "Hello, World!"}
await store.aput(namespace, item_id, item_value)
item = await store.aget(namespace, item_id)
assert item
assert item.namespace == namespace
assert item.key == item_id
assert item.value == item_value
updated_value = {
"title": "Updated Test Document",
"content": "Hello, LangGraph!",
}
await store.aput(namespace, item_id, updated_value)
updated_item = await store.aget(namespace, item_id)
assert updated_item.value == updated_value
assert updated_item.updated_at > item.updated_at
different_namespace = ("test", "other_documents")
item_in_different_namespace = await store.aget(different_namespace, item_id)
assert item_in_different_namespace is None
new_item_id = "doc2"
new_item_value = {"title": "Another Document", "content": "Greetings!"}
await store.aput(namespace, new_item_id, new_item_value)
search_results = await store.asearch(["test"], limit=10)
items = search_results
assert len(items) == 2
assert any(item.key == item_id for item in items)
assert any(item.key == new_item_id for item in items)
namespaces = await store.alist_namespaces(prefix=["test"])
assert ("test", "documents") in namespaces
await store.adelete(namespace, item_id)
await store.adelete(namespace, new_item_id)
deleted_item = await store.aget(namespace, item_id)
assert deleted_item is None
deleted_item = await store.aget(namespace, new_item_id)
assert deleted_item is None
empty_search_results = await store.asearch(["test"], limit=10)
assert len(empty_search_results) == 0
async def test_list_namespaces() -> None:
async with AsyncDuckDBStore.from_conn_string(":memory:") as store:
await store.setup()
test_pref = str(uuid.uuid4())
test_namespaces = [
(test_pref, "test", "documents", "public", test_pref),
(test_pref, "test", "documents", "private", test_pref),
(test_pref, "test", "images", "public", test_pref),
(test_pref, "test", "images", "private", test_pref),
(test_pref, "prod", "documents", "public", test_pref),
(
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
),
(test_pref, "prod", "documents", "private", test_pref),
]
for namespace in test_namespaces:
await store.aput(namespace, "dummy", {"content": "dummy"})
prefix_result = await store.alist_namespaces(prefix=[test_pref, "test"])
assert len(prefix_result) == 4
assert all([ns[1] == "test" for ns in prefix_result])
specific_prefix_result = await store.alist_namespaces(
prefix=[test_pref, "test", "documents"]
)
assert len(specific_prefix_result) == 2
assert all([ns[1:3] == ("test", "documents") for ns in specific_prefix_result])
suffix_result = await store.alist_namespaces(suffix=["public", test_pref])
assert len(suffix_result) == 4
assert all(ns[-2] == "public" for ns in suffix_result)
prefix_suffix_result = await store.alist_namespaces(
prefix=[test_pref, "test"], suffix=["public", test_pref]
)
assert len(prefix_suffix_result) == 2
assert all(
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
)
wildcard_prefix_result = await store.alist_namespaces(
prefix=[test_pref, "*", "documents"]
)
assert len(wildcard_prefix_result) == 5
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
wildcard_suffix_result = await store.alist_namespaces(
suffix=["*", "public", test_pref]
)
assert len(wildcard_suffix_result) == 4
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
wildcard_single = await store.alist_namespaces(
suffix=["some", "*", "public", test_pref]
)
assert len(wildcard_single) == 1
assert wildcard_single[0] == (
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
)
max_depth_result = await store.alist_namespaces(max_depth=3)
assert all([len(ns) <= 3 for ns in max_depth_result])
max_depth_result = await store.alist_namespaces(
max_depth=4, prefix=[test_pref, "*", "documents"]
)
assert (
len(set(tuple(res) for res in max_depth_result))
== len(max_depth_result)
== 5
)
limit_result = await store.alist_namespaces(prefix=[test_pref], limit=3)
assert len(limit_result) == 3
offset_result = await store.alist_namespaces(prefix=[test_pref], offset=3)
assert len(offset_result) == len(test_namespaces) - 3
empty_prefix_result = await store.alist_namespaces(prefix=[test_pref])
assert len(empty_prefix_result) == len(test_namespaces)
assert set(tuple(ns) for ns in empty_prefix_result) == set(
tuple(ns) for ns in test_namespaces
)
for namespace in test_namespaces:
await store.adelete(namespace, "dummy")
async def test_search():
async with AsyncDuckDBStore.from_conn_string(":memory:") as store:
await store.setup()
test_namespaces = [
("test_search", "documents", "user1"),
("test_search", "documents", "user2"),
("test_search", "reports", "department1"),
("test_search", "reports", "department2"),
]
test_items = [
{"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
{"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
]
empty = await store.asearch(
(
"scoped",
"assistant_id",
"shared",
"6c5356f6-63ab-4158-868d-cd9fd14c736e",
),
limit=10,
offset=0,
)
assert len(empty) == 0
for namespace, item in zip(test_namespaces, test_items):
await store.aput(namespace, f"item_{namespace[-1]}", item)
docs_result = await store.asearch(["test_search", "documents"])
assert len(docs_result) == 2
assert all([item.namespace[1] == "documents" for item in docs_result]), [
item.namespace for item in docs_result
]
reports_result = await store.asearch(["test_search", "reports"])
assert len(reports_result) == 2
assert all(item.namespace[1] == "reports" for item in reports_result)
limited_result = await store.asearch(["test_search"], limit=2)
assert len(limited_result) == 2
offset_result = await store.asearch(["test_search"])
assert len(offset_result) == 4
offset_result = await store.asearch(["test_search"], offset=2)
assert len(offset_result) == 2
assert all(item not in limited_result for item in offset_result)
john_doe_result = await store.asearch(
["test_search"], filter={"author": "John Doe"}
)
assert len(john_doe_result) == 2
assert all(item.value["author"] == "John Doe" for item in john_doe_result)
draft_result = await store.asearch(["test_search"], filter={"tags": ["draft"]})
assert len(draft_result) == 2
assert all("draft" in item.value["tags"] for item in draft_result)
page1 = await store.asearch(["test_search"], limit=2, offset=0)
page2 = await store.asearch(["test_search"], limit=2, offset=2)
all_items = page1 + page2
assert len(all_items) == 4
assert len(set(item.key for item in all_items)) == 4
empty = await store.asearch(
(
"scoped",
"assistant_id",
"shared",
"again",
"maybe",
"some-long",
"6be5cb0e-2eb4-42e6-bb6b-fba3c269db25",
),
limit=10,
offset=0,
)
assert len(empty) == 0
# Test with a namespace beginning with a number (like a UUID)
uuid_namespace = (str(uuid.uuid4()), "documents")
uuid_item_id = "uuid_doc"
uuid_item_value = {
"title": "UUID Document",
"content": "This document has a UUID namespace.",
}
# Insert the item with the UUID namespace
await store.aput(uuid_namespace, uuid_item_id, uuid_item_value)
# Retrieve the item to verify it was stored correctly
retrieved_item = await store.aget(uuid_namespace, uuid_item_id)
assert retrieved_item is not None
assert retrieved_item.namespace == uuid_namespace
assert retrieved_item.key == uuid_item_id
assert retrieved_item.value == uuid_item_value
# Search for the item using the UUID namespace
search_result = await store.asearch([uuid_namespace[0]])
assert len(search_result) == 1
assert search_result[0].key == uuid_item_id
assert search_result[0].value == uuid_item_value
# Clean up: delete the item with the UUID namespace
await store.adelete(uuid_namespace, uuid_item_id)
# Verify the item was deleted
deleted_item = await store.aget(uuid_namespace, uuid_item_id)
assert deleted_item is None
for namespace in test_namespaces:
await store.adelete(namespace, f"item_{namespace[-1]}")
-457
View File
@@ -1,457 +0,0 @@
# type: ignore
import uuid
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock
import pytest
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.duckdb import DuckDBStore
class MockCursor:
def __init__(self, fetch_result: Any) -> None:
self.fetch_result = fetch_result
self.execute = MagicMock()
self.fetchall = MagicMock(return_value=self.fetch_result)
class MockConnection:
def __init__(self) -> None:
self.cursor = MagicMock()
@pytest.fixture
def mock_connection() -> MockConnection:
return MockConnection()
@pytest.fixture
def store(mock_connection: MockConnection) -> DuckDBStore:
duck_db_store = DuckDBStore(mock_connection)
duck_db_store.setup()
return duck_db_store
def test_batch_order(store: DuckDBStore) -> None:
mock_connection = store.conn
mock_get_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_search_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
]
)
mock_list_namespaces_cursor = MockCursor(
[
("test",),
]
)
failures = []
def cursor_side_effect() -> Any:
cursor = MagicMock()
def execute_side_effect(query: str, *params: Any) -> None:
# My super sophisticated database.
if "WHERE prefix = ? AND key" in query:
cursor.fetchall = mock_get_cursor.fetchall
elif "SELECT prefix, key, value" in query:
cursor.fetchall = mock_search_cursor.fetchall
elif "SELECT DISTINCT ON (truncated_prefix)" in query:
cursor.fetchall = mock_list_namespaces_cursor.fetchall
elif "INSERT INTO " in query:
pass
else:
e = ValueError(f"Unmatched query: {query}")
failures.append(e)
raise e
cursor.execute = MagicMock(side_effect=execute_side_effect)
return cursor
mock_connection.cursor.side_effect = cursor_side_effect
ops = [
GetOp(namespace=("test",), key="key1"),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
results = store.batch(ops)
assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
assert results[0].value == {"data": "value1"}
assert results[0].key == "key1"
assert results[1] is None
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
assert results[3] == [("test",)]
assert results[4] is None
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
GetOp(namespace=("test",), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
GetOp(namespace=("test",), key="key1"),
]
results_reordered = store.batch(ops_reordered)
assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
assert len(results_reordered[0]) == 1
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
assert results_reordered[2] == [("test",)]
assert results_reordered[3] is None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
assert results_reordered[4].key == "key1"
def test_batch_get_ops(store: DuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_connection.cursor.return_value = mock_cursor
ops = [
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test",), key="key3"),
]
results = store.batch(ops)
assert len(results) == 3
assert results[0] is not None
assert results[1] is not None
assert results[2] is None
assert results[0].key == "key1"
assert results[1].key == "key2"
def test_batch_put_ops(store: DuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor([])
mock_connection.cursor.return_value = mock_cursor
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
PutOp(namespace=("test",), key="key3", value=None),
]
results = store.batch(ops)
assert len(results) == 3
assert all(result is None for result in results)
assert mock_cursor.execute.call_count == 2
def test_batch_search_ops(store: DuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
(
"test.foo",
"key1",
'{"data": "value1"}',
datetime.now(),
datetime.now(),
),
(
"test.bar",
"key2",
'{"data": "value2"}',
datetime.now(),
datetime.now(),
),
]
)
mock_connection.cursor.return_value = mock_cursor
ops = [
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
]
results = store.batch(ops)
assert len(results) == 2
assert len(results[0]) == 2
assert len(results[1]) == 2
def test_batch_list_namespaces_ops(store: DuckDBStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor([("test.namespace1",), ("test.namespace2",)])
mock_connection.cursor.return_value = mock_cursor
ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
results = store.batch(ops)
assert len(results) == 1
assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
def test_basic_store_ops() -> None:
with DuckDBStore.from_conn_string(":memory:") as store:
store.setup()
namespace = ("test", "documents")
item_id = "doc1"
item_value = {"title": "Test Document", "content": "Hello, World!"}
store.put(namespace, item_id, item_value)
item = store.get(namespace, item_id)
assert item
assert item.namespace == namespace
assert item.key == item_id
assert item.value == item_value
updated_value = {
"title": "Updated Test Document",
"content": "Hello, LangGraph!",
}
store.put(namespace, item_id, updated_value)
updated_item = store.get(namespace, item_id)
assert updated_item.value == updated_value
assert updated_item.updated_at > item.updated_at
different_namespace = ("test", "other_documents")
item_in_different_namespace = store.get(different_namespace, item_id)
assert item_in_different_namespace is None
new_item_id = "doc2"
new_item_value = {"title": "Another Document", "content": "Greetings!"}
store.put(namespace, new_item_id, new_item_value)
search_results = store.search(["test"], limit=10)
items = search_results
assert len(items) == 2
assert any(item.key == item_id for item in items)
assert any(item.key == new_item_id for item in items)
namespaces = store.list_namespaces(prefix=["test"])
assert ("test", "documents") in namespaces
store.delete(namespace, item_id)
store.delete(namespace, new_item_id)
deleted_item = store.get(namespace, item_id)
assert deleted_item is None
deleted_item = store.get(namespace, new_item_id)
assert deleted_item is None
empty_search_results = store.search(["test"], limit=10)
assert len(empty_search_results) == 0
def test_list_namespaces() -> None:
with DuckDBStore.from_conn_string(":memory:") as store:
store.setup()
test_pref = str(uuid.uuid4())
test_namespaces = [
(test_pref, "test", "documents", "public", test_pref),
(test_pref, "test", "documents", "private", test_pref),
(test_pref, "test", "images", "public", test_pref),
(test_pref, "test", "images", "private", test_pref),
(test_pref, "prod", "documents", "public", test_pref),
(
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
),
(test_pref, "prod", "documents", "private", test_pref),
]
for namespace in test_namespaces:
store.put(namespace, "dummy", {"content": "dummy"})
prefix_result = store.list_namespaces(prefix=[test_pref, "test"])
assert len(prefix_result) == 4
assert all([ns[1] == "test" for ns in prefix_result])
specific_prefix_result = store.list_namespaces(
prefix=[test_pref, "test", "documents"]
)
assert len(specific_prefix_result) == 2
assert all([ns[1:3] == ("test", "documents") for ns in specific_prefix_result])
suffix_result = store.list_namespaces(suffix=["public", test_pref])
assert len(suffix_result) == 4
assert all(ns[-2] == "public" for ns in suffix_result)
prefix_suffix_result = store.list_namespaces(
prefix=[test_pref, "test"], suffix=["public", test_pref]
)
assert len(prefix_suffix_result) == 2
assert all(
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
)
wildcard_prefix_result = store.list_namespaces(
prefix=[test_pref, "*", "documents"]
)
assert len(wildcard_prefix_result) == 5
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
wildcard_suffix_result = store.list_namespaces(
suffix=["*", "public", test_pref]
)
assert len(wildcard_suffix_result) == 4
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
wildcard_single = store.list_namespaces(
suffix=["some", "*", "public", test_pref]
)
assert len(wildcard_single) == 1
assert wildcard_single[0] == (
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
)
max_depth_result = store.list_namespaces(max_depth=3)
assert all([len(ns) <= 3 for ns in max_depth_result])
max_depth_result = store.list_namespaces(
max_depth=4, prefix=[test_pref, "*", "documents"]
)
assert (
len(set(tuple(res) for res in max_depth_result))
== len(max_depth_result)
== 5
)
limit_result = store.list_namespaces(prefix=[test_pref], limit=3)
assert len(limit_result) == 3
offset_result = store.list_namespaces(prefix=[test_pref], offset=3)
assert len(offset_result) == len(test_namespaces) - 3
empty_prefix_result = store.list_namespaces(prefix=[test_pref])
assert len(empty_prefix_result) == len(test_namespaces)
assert set(tuple(ns) for ns in empty_prefix_result) == set(
tuple(ns) for ns in test_namespaces
)
for namespace in test_namespaces:
store.delete(namespace, "dummy")
def test_search():
with DuckDBStore.from_conn_string(":memory:") as store:
store.setup()
test_namespaces = [
("test_search", "documents", "user1"),
("test_search", "documents", "user2"),
("test_search", "reports", "department1"),
("test_search", "reports", "department2"),
]
test_items = [
{"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
{"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
]
for namespace, item in zip(test_namespaces, test_items):
store.put(namespace, f"item_{namespace[-1]}", item)
docs_result = store.search(["test_search", "documents"])
assert len(docs_result) == 2
assert all(
[item.namespace[1] == "documents" for item in docs_result]
), docs_result
reports_result = store.search(["test_search", "reports"])
assert len(reports_result) == 2
assert all(item.namespace[1] == "reports" for item in reports_result)
limited_result = store.search(["test_search"], limit=2)
assert len(limited_result) == 2
offset_result = store.search(["test_search"])
assert len(offset_result) == 4
offset_result = store.search(["test_search"], offset=2)
assert len(offset_result) == 2
assert all(item not in limited_result for item in offset_result)
john_doe_result = store.search(["test_search"], filter={"author": "John Doe"})
assert len(john_doe_result) == 2
assert all(item.value["author"] == "John Doe" for item in john_doe_result)
draft_result = store.search(["test_search"], filter={"tags": ["draft"]})
assert len(draft_result) == 2
assert all("draft" in item.value["tags"] for item in draft_result)
page1 = store.search(["test_search"], limit=2, offset=0)
page2 = store.search(["test_search"], limit=2, offset=2)
all_items = page1 + page2
assert len(all_items) == 4
assert len(set(item.key for item in all_items)) == 4
for namespace in test_namespaces:
store.delete(namespace, f"item_{namespace[-1]}")
-111
View File
@@ -1,111 +0,0 @@
from typing import Any
import pytest
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.duckdb import DuckDBSaver
class TestDuckDBSaver:
@pytest.fixture(autouse=True)
def setup(self) -> None:
# objects for test setup
self.config_1: RunnableConfig = {
"configurable": {
"thread_id": "thread-1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_ns": "",
}
}
self.config_2: RunnableConfig = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_id": "2",
"checkpoint_ns": "",
}
}
self.config_3: RunnableConfig = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_id": "2-inner",
"checkpoint_ns": "inner",
}
}
self.chkpnt_1: Checkpoint = empty_checkpoint()
self.chkpnt_2: Checkpoint = create_checkpoint(self.chkpnt_1, {}, 1)
self.chkpnt_3: Checkpoint = empty_checkpoint()
self.metadata_1: CheckpointMetadata = {
"source": "input",
"step": 2,
"writes": {},
"score": 1,
}
self.metadata_2: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
self.metadata_3: CheckpointMetadata = {}
def test_search(self) -> None:
with DuckDBSaver.from_conn_string(":memory:") as saver:
saver.setup()
# save checkpoints
saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {})
saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {})
saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {})
# call method / assertions
query_1 = {"source": "input"} # search by 1 key
query_2 = {
"step": 1,
"writes": {"foo": "bar"},
} # search by multiple keys
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
query_4 = {"source": "update", "step": 1} # no match
search_results_1 = list(saver.list(None, filter=query_1))
assert len(search_results_1) == 1
assert search_results_1[0].metadata == self.metadata_1
search_results_2 = list(saver.list(None, filter=query_2))
assert len(search_results_2) == 1
assert search_results_2[0].metadata == self.metadata_2
search_results_3 = list(saver.list(None, filter=query_3))
assert len(search_results_3) == 3
search_results_4 = list(saver.list(None, filter=query_4))
assert len(search_results_4) == 0
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = list(
saver.list({"configurable": {"thread_id": "thread-2"}})
)
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
def test_null_chars(self) -> None:
with DuckDBSaver.from_conn_string(":memory:") as saver:
saver.setup()
config = saver.put(self.config_1, self.chkpnt_1, {"my_key": "\x00abc"}, {})
assert saver.get_tuple(config).metadata["my_key"] == "abc" # type: ignore
assert (
list(saver.list(None, filter={"my_key": "abc"}))[0].metadata["my_key"] # type: ignore
== "abc"
)
@@ -327,6 +327,7 @@ class PostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -350,6 +351,7 @@ class PostgresSaver(BasePostgresSaver):
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
task_path,
writes,
),
)
@@ -285,6 +285,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint asynchronously.
@@ -306,6 +307,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
task_path,
writes,
)
async with self._cursor(pipeline=True) as cur:
@@ -462,6 +464,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -471,9 +474,10 @@ class AsyncPostgresSaver(BasePostgresSaver):
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
"""
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
self.aput_writes(config, writes, task_id, task_path), self.loop
).result()
@@ -57,6 +57,9 @@ MIGRATIONS = [
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
);""",
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
# NOTE: this is a no-op migration to ensure that the versions in the migrations table are correct.
# This is necessary due to an empty migration previously added to the list.
"SELECT 1;",
"""
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
""",
@@ -66,6 +69,7 @@ MIGRATIONS = [
"""
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
""",
"""ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
]
SELECT_SQL = f"""
@@ -94,7 +98,7 @@ select
and cw.checkpoint_id = checkpoints.checkpoint_id
) as pending_writes,
(
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_path, cw.task_id, cw.idx)
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
@@ -119,8 +123,8 @@ UPSERT_CHECKPOINTS_SQL = """
"""
UPSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
channel = EXCLUDED.channel,
type = EXCLUDED.type,
@@ -128,8 +132,8 @@ UPSERT_CHECKPOINT_WRITES_SQL = """
"""
INSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
"""
@@ -220,14 +224,16 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
checkpoint_ns: str,
checkpoint_id: str,
task_id: str,
task_path: str,
writes: Sequence[tuple[str, Any]],
) -> list[tuple[str, str, str, str, int, str, str, bytes]]:
) -> list[tuple[str, str, str, str, str, int, str, str, bytes]]:
return [
(
thread_id,
checkpoint_ns,
checkpoint_id,
task_id,
task_path,
WRITES_IDX_MAP.get(channel, idx),
channel,
*self.serde.dumps_typed(value),
@@ -74,6 +74,9 @@ MIGRATIONS = [
"""
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
""",
"""
ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
""",
]
SELECT_SQL = f"""
@@ -99,7 +102,7 @@ select
and cw.checkpoint_id = (checkpoint->>'id')
) as pending_writes,
(
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_path, cw.task_id, cw.idx)
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
@@ -125,8 +128,8 @@ UPSERT_CHECKPOINTS_SQL = """
"""
UPSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
channel = EXCLUDED.channel,
type = EXCLUDED.type,
@@ -134,8 +137,8 @@ UPSERT_CHECKPOINT_WRITES_SQL = """
"""
INSERT_CHECKPOINT_WRITES_SQL = """
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
"""
@@ -430,6 +433,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -453,6 +457,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
task_path,
writes,
),
)
@@ -747,6 +752,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint asynchronously.
@@ -768,6 +774,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config["configurable"]["checkpoint_ns"],
config["configurable"]["checkpoint_id"],
task_id,
task_path,
writes,
)
async with self._cursor(pipeline=True) as cur:
@@ -903,6 +910,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -912,7 +920,8 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
"""
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
self.aput_writes(config, writes, task_id, task_path), self.loop
).result()
@@ -39,14 +39,14 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
"""Asynchronous Postgres-backed store with optional vector search using pgvector.
!!! example "Examples"
Basic setup and key-value storage:
Basic setup and usage:
```python
from langgraph.store.postgres import AsyncPostgresStore
async with AsyncPostgresStore.from_conn_string(
"postgresql://user:pass@localhost:5432/dbname"
) as store:
await store.setup()
conn_string = "postgresql://user:pass@localhost:5432/dbname"
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
await store.setup() # Run migrations. Done once
# Store and retrieve data
await store.aput(("users", "123"), "prefs", {"theme": "dark"})
@@ -58,38 +58,41 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
from langchain.embeddings import init_embeddings
from langgraph.store.postgres import AsyncPostgresStore
conn_string = "postgresql://user:pass@localhost:5432/dbname"
async with AsyncPostgresStore.from_conn_string(
"postgresql://user:pass@localhost:5432/dbname",
conn_string,
index={
"dims": 1536,
"embed": init_embeddings("openai:text-embedding-3-small"),
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
}
) as store:
await store.setup() # Do this once to run migrations
await store.setup() # Run migrations. Done once
# Store documents
await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
# Don't index the following
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False)
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
# Search by similarity
results = await store.asearch(("docs",), query="python programming")
results = await store.asearch(("docs",), "programming guides", limit=2)
```
Using connection pooling for better performance:
```python
from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
conn_string = "postgresql://user:pass@localhost:5432/dbname"
async with AsyncPostgresStore.from_conn_string(
"postgresql://user:pass@localhost:5432/dbname",
conn_string,
pool_config=PoolConfig(
min_size=5,
max_size=20
)
) as store:
await store.setup()
await store.setup() # Run migrations. Done once
# Use store with connection pooling...
```
@@ -102,7 +105,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
Note:
Semantic search is disabled by default. You can enable it by providing an `index` configuration
when creating the store. Without this configuration, all `index` arguments passed to
`put` or `aput`will have no effect.
`put` or `aput` will have no effect.
"""
__slots__ = (
@@ -536,18 +536,35 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
"""Postgres-backed store with optional vector search using pgvector.
!!! example "Examples"
Basic setup and key-value storage:
Basic setup and usage:
```python
from langgraph.store.postgres import PostgresStore
from psycopg import Connection
conn_string = "postgresql://user:pass@localhost:5432/dbname"
# Using direct connection
with Connection.connect(conn_string) as conn:
store = PostgresStore(conn)
store.setup() # Run migrations. Done once
# Store and retrieve data
store.put(("users", "123"), "prefs", {"theme": "dark"})
item = store.get(("users", "123"), "prefs")
```
Or using the convenient from_conn_string helper:
```python
from langgraph.store.postgres import PostgresStore
store = PostgresStore(
connection_string="postgresql://user:pass@localhost:5432/dbname"
)
store.setup()
conn_string = "postgresql://user:pass@localhost:5432/dbname"
# Store and retrieve data
store.put(("users", "123"), "prefs", {"theme": "dark"})
item = store.get(("users", "123"), "prefs")
with PostgresStore.from_conn_string(conn_string) as store:
store.setup()
# Store and retrieve data
store.put(("users", "123"), "prefs", {"theme": "dark"})
item = store.get(("users", "123"), "prefs")
```
Vector search using LangChain embeddings:
@@ -555,23 +572,25 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
from langchain.embeddings import init_embeddings
from langgraph.store.postgres import PostgresStore
store = PostgresStore(
connection_string="postgresql://user:pass@localhost:5432/dbname",
conn_string = "postgresql://user:pass@localhost:5432/dbname"
with PostgresStore.from_conn_string(
conn_string,
index={
"dims": 1536,
"embed": init_embeddings("openai:text-embedding-3-small"),
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
}
)
store.setup() # Do this once to run migrations
) as store:
store.setup() # Do this once to run migrations
# Store documents
store.put(("docs",), "doc1", {"text": "Python tutorial"})
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
# Store documents
store.put(("docs",), "doc1", {"text": "Python tutorial"})
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
# Search by similarity
results = store.search(("docs",), query="python programming")
# Search by similarity
results = store.search(("docs",), "programming guides", limit=2)
```
Note:
+52 -4
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -6,6 +6,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -17,6 +18,7 @@ version = "4.7.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
@@ -39,6 +41,7 @@ version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
@@ -50,6 +53,7 @@ version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["main", "dev"]
files = [
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
@@ -164,6 +168,7 @@ version = "2.3.0"
description = "Codespell"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
{file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
@@ -181,6 +186,7 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -192,6 +198,7 @@ version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
@@ -202,6 +209,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -216,6 +225,7 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -227,6 +237,7 @@ version = "1.0.7"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
@@ -248,6 +259,7 @@ version = "0.28.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"},
{file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"},
@@ -272,6 +284,7 @@ version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
@@ -286,6 +299,7 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -297,6 +311,7 @@ version = "1.33"
description = "Apply JSON-Patches (RFC 6902)"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
groups = ["main", "dev"]
files = [
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
@@ -311,6 +326,7 @@ version = "3.0.0"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
@@ -322,6 +338,7 @@ version = "0.3.21"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main", "dev"]
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"},
@@ -341,10 +358,11 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.8"
version = "2.0.10"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
groups = ["main", "dev"]
files = []
develop = true
@@ -362,6 +380,7 @@ version = "0.1.147"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
groups = ["main", "dev"]
files = [
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
@@ -386,6 +405,7 @@ version = "1.1.0"
description = "MessagePack serializer"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"},
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"},
@@ -459,6 +479,7 @@ version = "1.13.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
@@ -512,6 +533,7 @@ version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -523,6 +545,7 @@ version = "3.10.12"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
@@ -607,6 +630,7 @@ version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
@@ -618,6 +642,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -633,6 +658,7 @@ version = "3.2.3"
description = "PostgreSQL database adapter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"},
{file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"},
@@ -657,6 +683,8 @@ version = "3.2.3"
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "implementation_name != \"pypy\""
files = [
{file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"},
{file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"},
@@ -730,6 +758,7 @@ version = "3.2.4"
description = "Connection Pool for Psycopg"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"},
{file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"},
@@ -744,6 +773,7 @@ version = "2.10.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
@@ -764,6 +794,7 @@ version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
@@ -876,6 +907,7 @@ version = "7.4.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
@@ -898,6 +930,7 @@ version = "0.21.2"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
{file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
@@ -916,6 +949,7 @@ version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
@@ -933,6 +967,7 @@ version = "4.2.0"
description = "Local continuous test runner with pytest and watchdog."
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"},
]
@@ -949,6 +984,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
@@ -1011,6 +1047,7 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -1032,6 +1069,7 @@ version = "1.0.0"
description = "A utility belt for advanced users of python-requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main", "dev"]
files = [
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
@@ -1046,6 +1084,7 @@ version = "0.6.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
@@ -1073,6 +1112,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -1084,6 +1124,7 @@ version = "9.0.0"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"},
{file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"},
@@ -1099,6 +1140,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -1140,6 +1183,7 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -1151,6 +1195,8 @@ version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main", "dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
@@ -1162,6 +1208,7 @@ version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
@@ -1179,6 +1226,7 @@ version = "6.0.0"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
@@ -1216,6 +1264,6 @@ files = [
watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = "^3.9.0,<4.0"
content-hash = "d64fe96797a79103d952c13d4dc0a0296c468b6615b4872aa01cb34479ca4104"
content-hash = "61326e4e81a4e8854763a119f39d4f5d0a54cee868b4dbc91b95ce7d2cebba5b"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.11"
version = "2.0.13"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.7"
langgraph-checkpoint = "^2.0.10"
orjson = ">=3.10.1"
psycopg = "^3.2.0"
psycopg-pool = "^3.2.0"
@@ -424,6 +424,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -433,6 +434,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
"""
query = (
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
@@ -1,17 +1,8 @@
import asyncio
import random
from collections.abc import AsyncIterator, Iterator, Sequence
from contextlib import asynccontextmanager
from typing import (
Any,
AsyncIterator,
Callable,
Dict,
Iterator,
Optional,
Sequence,
Tuple,
TypeVar,
)
from typing import Any, Callable, Optional, TypeVar
import aiosqlite
from langchain_core.runnables import RunnableConfig
@@ -173,7 +164,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self,
config: Optional[RunnableConfig],
*,
filter: Optional[Dict[str, Any]] = None,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
@@ -207,7 +198,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_),
anext(aiter_), # noqa: F821
self.loop,
).result()
except StopAsyncIteration:
@@ -239,10 +230,14 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
).result()
def put_writes(
self, config: RunnableConfig, writes: Sequence[Tuple[str, Any]], task_id: str
self,
config: RunnableConfig,
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
self.aput_writes(config, writes, task_id, task_path), self.loop
).result()
async def setup(self) -> None:
@@ -372,7 +367,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self,
config: Optional[RunnableConfig],
*,
filter: Optional[Dict[str, Any]] = None,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> AsyncIterator[CheckpointTuple]:
@@ -398,9 +393,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
ORDER BY checkpoint_id DESC"""
if limit:
query += f" LIMIT {limit}"
async with self.lock, self.conn.execute(
query, params
) as cur, self.conn.cursor() as wcur:
async with (
self.lock,
self.conn.execute(query, params) as cur,
self.conn.cursor() as wcur,
):
async for (
thread_id,
checkpoint_ns,
@@ -467,16 +464,19 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(metadata)
async with self.lock, self.conn.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
str(config["configurable"]["thread_id"]),
checkpoint_ns,
checkpoint["id"],
config["configurable"].get("checkpoint_id"),
type_,
serialized_checkpoint,
serialized_metadata,
async with (
self.lock,
self.conn.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
str(config["configurable"]["thread_id"]),
checkpoint_ns,
checkpoint["id"],
config["configurable"].get("checkpoint_id"),
type_,
serialized_checkpoint,
serialized_metadata,
),
),
):
await self.conn.commit()
@@ -491,8 +491,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
async def aput_writes(
self,
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint asynchronously.
@@ -502,6 +503,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
"""
query = (
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
+46 -4
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
[[package]]
name = "aiosqlite"
@@ -6,6 +6,7 @@ version = "0.20.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
@@ -24,6 +25,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -35,6 +37,7 @@ version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
@@ -57,6 +60,7 @@ version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
files = [
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
@@ -68,6 +72,7 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["main", "dev"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -167,6 +172,7 @@ version = "2.3.0"
description = "Codespell"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
{file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
@@ -184,6 +190,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -195,6 +203,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -209,6 +219,7 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -220,6 +231,7 @@ version = "1.0.5"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
@@ -241,6 +253,7 @@ version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
@@ -266,6 +279,7 @@ version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
groups = ["main", "dev"]
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
@@ -277,6 +291,7 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -288,6 +303,7 @@ version = "1.33"
description = "Apply JSON-Patches (RFC 6902)"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
groups = ["main", "dev"]
files = [
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
@@ -302,6 +318,7 @@ version = "3.0.0"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
@@ -313,6 +330,7 @@ version = "0.3.0"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main", "dev"]
files = [
{file = "langchain_core-0.3.0-py3-none-any.whl", hash = "sha256:bee6dae2366d037ef0c5b87401fed14b5497cad26f97724e8c9ca7bc9239e847"},
{file = "langchain_core-0.3.0.tar.gz", hash = "sha256:1249149ea3ba24c9c761011483c14091573a5eb1a773aa0db9c8ad155dd4a69d"},
@@ -332,10 +350,11 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.2"
version = "2.0.10"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
groups = ["main", "dev"]
files = []
develop = true
@@ -353,6 +372,7 @@ version = "0.1.120"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
groups = ["main", "dev"]
files = [
{file = "langsmith-0.1.120-py3-none-any.whl", hash = "sha256:54d2785e301646c0988e0a69ebe4d976488c87b41928b358cb153b6ddd8db62b"},
{file = "langsmith-0.1.120.tar.gz", hash = "sha256:25499ca187b41bd89d784b272b97a8d76f60e0e21bdf20336e8a2aa6a9b23ac9"},
@@ -373,6 +393,7 @@ version = "1.1.0"
description = "MessagePack serializer"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"},
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"},
@@ -446,6 +467,7 @@ version = "1.11.2"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
{file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
@@ -493,6 +515,7 @@ version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -504,6 +527,7 @@ version = "3.10.6"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
@@ -566,6 +590,7 @@ version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
@@ -577,6 +602,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -592,6 +618,7 @@ version = "2.8.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
@@ -614,6 +641,7 @@ version = "2.20.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
@@ -715,6 +743,7 @@ version = "7.4.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
@@ -737,6 +766,7 @@ version = "0.21.2"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
{file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
@@ -755,6 +785,7 @@ version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
@@ -772,6 +803,7 @@ version = "0.4.2"
description = "Automatically rerun your tests on file modifications"
optional = false
python-versions = "<4.0.0,>=3.7.0"
groups = ["dev"]
files = [
{file = "pytest_watcher-0.4.2-py3-none-any.whl", hash = "sha256:a43949ba67dd8d7e1fd0de5eea44a999081f0aec9f93b4e744264b4c6a3d9bbe"},
{file = "pytest_watcher-0.4.2.tar.gz", hash = "sha256:7b292f025ca19617cd7567c228c6187b5087f2da9e4d2cf6e144e5764a0471b0"},
@@ -787,6 +819,7 @@ version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
@@ -847,6 +880,7 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -868,6 +902,7 @@ version = "0.6.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
@@ -895,6 +930,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -906,6 +942,7 @@ version = "8.5.0"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
@@ -921,6 +958,8 @@ version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -932,6 +971,7 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -943,6 +983,7 @@ version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
@@ -960,6 +1001,7 @@ version = "4.0.1"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
@@ -999,6 +1041,6 @@ files = [
watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = "^3.9.0"
content-hash = "927b49b9ba72a301980237d7adc2e73cdacfbe127a174c7488136a9af9372796"
content-hash = "03c697eae6f550f3c7e29f1d61f4c409dabe04ae8d43281728e549174d2fc670"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "2.0.2"
version = "2.0.3"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^2.0.2"
langgraph-checkpoint = "^2.0.10"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
@@ -1,16 +1,13 @@
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
from datetime import datetime, timezone
from typing import (
from typing import ( # noqa: UP035
Any,
AsyncIterator,
Dict,
Generic,
Iterator,
List,
Literal,
Mapping,
NamedTuple,
Optional,
Sequence,
Tuple,
TypedDict,
TypeVar,
@@ -305,6 +302,7 @@ class BaseCheckpointSaver(Generic[V]):
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Store intermediate writes linked to a checkpoint.
@@ -312,6 +310,7 @@ class BaseCheckpointSaver(Generic[V]):
config (RunnableConfig): Configuration of the related checkpoint.
writes (List[Tuple[str, Any]]): List of writes to store.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -397,6 +396,7 @@ class BaseCheckpointSaver(Generic[V]):
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Asynchronously store intermediate writes linked to a checkpoint.
@@ -404,6 +404,7 @@ class BaseCheckpointSaver(Generic[V]):
config (RunnableConfig): Configuration of the related checkpoint.
writes (List[Tuple[str, Any]]): List of writes to store.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -4,9 +4,10 @@ import pickle
import random
import shutil
from collections import defaultdict
from collections.abc import AsyncIterator, Iterator, Sequence
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
from types import TracebackType
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
@@ -65,14 +66,15 @@ class MemorySaver(
],
]
writes: defaultdict[
tuple[str, str, str], dict[tuple[str, int], tuple[str, str, tuple[str, bytes]]]
tuple[str, str, str],
dict[tuple[str, int], tuple[str, str, tuple[str, bytes], str]],
]
def __init__(
self,
*,
serde: Optional[SerializerProtocol] = None,
factory: Type[defaultdict] = defaultdict,
factory: type[defaultdict] = defaultdict,
) -> None:
super().__init__(serde=serde)
self.storage = factory(lambda: defaultdict(dict))
@@ -125,24 +127,27 @@ class MemorySaver(
checkpoint, metadata, parent_checkpoint_id = saved
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
sends = sorted(
(
(*w, k[1])
for k, w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].items()
if w[1] == TASKS
),
key=lambda w: (w[3], w[0], w[4]),
)
else:
sends = []
return CheckpointTuple(
config=config,
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
"pending_sends": [self.serde.loads_typed(s[2]) for s in sends],
},
metadata=self.serde.loads_typed(metadata),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
parent_config={
"configurable": {
@@ -160,13 +165,16 @@ class MemorySaver(
checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id]
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
sends = sorted(
(
(*w, k[1])
for k, w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].items()
if w[1] == TASKS
),
key=lambda w: (w[3], w[0], w[4]),
)
else:
sends = []
return CheckpointTuple(
@@ -179,11 +187,11 @@ class MemorySaver(
},
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
"pending_sends": [self.serde.loads_typed(s[2]) for s in sends],
},
metadata=self.serde.loads_typed(metadata),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
parent_config={
"configurable": {
@@ -200,7 +208,7 @@ class MemorySaver(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[Dict[str, Any]] = None,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
@@ -271,13 +279,16 @@ class MemorySaver(
].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
sends = sorted(
(
(*w, k[1])
for k, w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].items()
if w[1] == TASKS
),
key=lambda w: (w[3], w[0], w[4]),
)
else:
sends = []
@@ -291,7 +302,9 @@ class MemorySaver(
},
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
"pending_sends": [
self.serde.loads_typed(s[2]) for s in sends
],
},
metadata=metadata,
parent_config={
@@ -304,7 +317,7 @@ class MemorySaver(
if parent_checkpoint_id
else None,
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
(id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
],
)
@@ -353,8 +366,9 @@ class MemorySaver(
def put_writes(
self,
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Save a list of writes to the in-memory storage.
@@ -365,6 +379,7 @@ class MemorySaver(
config (RunnableConfig): The config to associate with the writes.
writes (list[tuple[str, Any]]): The writes to save.
task_id (str): Identifier for the task creating the writes.
task_path (str): Path of the task creating the writes.
Returns:
RunnableConfig: The updated config containing the saved writes' timestamp.
@@ -379,7 +394,12 @@ class MemorySaver(
if inner_key[1] >= 0 and outer_writes_ and inner_key in outer_writes_:
continue
self.writes[outer_key][inner_key] = (task_id, c, self.serde.dumps_typed(v))
self.writes[outer_key][inner_key] = (
task_id,
c,
self.serde.dumps_typed(v),
task_path,
)
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Asynchronous version of get_tuple.
@@ -399,7 +419,7 @@ class MemorySaver(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[Dict[str, Any]] = None,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> AsyncIterator[CheckpointTuple]:
@@ -440,8 +460,9 @@ class MemorySaver(
async def aput_writes(
self,
config: RunnableConfig,
writes: Sequence[Tuple[str, Any]],
writes: Sequence[tuple[str, Any]],
task_id: str,
task_path: str = "",
) -> None:
"""Asynchronous version of put_writes.
@@ -452,9 +473,12 @@ 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)
task_path (str): Path of the task creating the writes.
Returns:
None
"""
return self.put_writes(config, writes, task_id)
return self.put_writes(config, writes, task_id, task_path)
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
if current is None:
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.9"
version = "2.0.10"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+5
View File
@@ -605,6 +605,11 @@ def dev(
) from None
config_json = langgraph_cli.config.validate_config_file(pathlib.Path(config))
if config_json.get("node_version"):
raise click.UsageError(
"In-mem server for JS graphs is not supported in this version of the LangGraph CLI. Please use `npx @langchain/langgraph-cli` instead."
) from None
cwd = os.getcwd()
sys.path.append(cwd)
dependencies = config_json.get("dependencies", [])
+4 -1
View File
@@ -469,10 +469,11 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
except OSError:
return False
npm, yarn, pnpm = [
npm, yarn, pnpm, bun = [
test_file("package-lock.json"),
test_file("yarn.lock"),
test_file("pnpm-lock.yaml"),
test_file("bun.lockb"),
]
if yarn:
@@ -481,6 +482,8 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
install_cmd = "pnpm i --frozen-lockfile"
elif npm:
install_cmd = "npm ci"
elif bun:
install_cmd = "bun i"
else:
install_cmd = "npm i"
store_config = config.get("store")
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.67"
version = "0.1.68"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+175 -82
View File
@@ -12,25 +12,48 @@
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
### Why use LangGraph?
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
LangGraph provides fine-grained control over both the flow and state of your
agent applications. It implements a central
[persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/),
enabling features that are common to most agent architectures:
### Key Features
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
- **Cycles and Branching**: Implement loops and conditionals in your apps.
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
@@ -47,9 +70,7 @@ pip install -U langgraph
## Example
One of the central concepts of LangGraph is state. Each graph execution creates a state that is passed between nodes in the graph as they execute, and each node updates this internal state with its return value after it executes. The way that the graph updates its internal state is defined by either the type of graph chosen or a custom function.
Let's take a look at a simple example of an agent that can use a search tool.
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
```shell
pip install langchain-anthropic
@@ -66,10 +87,72 @@ export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_sk_...
```
```python
from typing import Annotated, Literal, TypedDict
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
<details open>
<summary>High-level implementation</summary>
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
tools = [search]
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
app = create_react_agent(model, tools, checkpointer=checkpointer)
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
> LangGraph is a **low-level** framework that allows you to implement any custom agent
architectures. Click on the low-level implementation below to see how to implement a
tool-calling agent from scratch.
<details>
<summary>Low-level implementation</summary>
```python
from typing import Literal
from langchain_core.messages import HumanMessage
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
@@ -91,7 +174,7 @@ tools = [search]
tool_node = ToolNode(tools)
model = ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0).bind_tools(tools)
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
# Define the function that determines whether to continue or not
def should_continue(state: MessagesState) -> Literal["tools", END]:
@@ -145,92 +228,102 @@ checkpointer = MemorySaver()
# Note that we're (optionally) passing the memory when compiling the graph
app = workflow.compile(checkpointer=checkpointer)
# Use the Runnable
# Use the agent
final_state = app.invoke(
{"messages": [HumanMessage(content="what is the weather in sf")]},
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
<b>Step-by-step Breakdown</b>:
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/modules/agents/tools/custom_tools">here</a>.
</li>
</ul>
</details>
```python
final_state = app.invoke(
{"messages": [HumanMessage(content="what about ny")]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<details>
<summary>Initialize graph with state.</summary>
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
### Step-by-step Breakdown
<details>
<summary>Define graph nodes.</summary>
1. <details>
<summary>Initialize the model and tools.</summary>
There are two main nodes we need:
- we use `ChatAnthropic` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
2. <details>
<summary>Initialize graph with state.</summary>
<details>
<summary>Define entry point and graph edges.</summary>
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
First, we need to set the entry point for graph execution - <code>agent</code> node.
3. <details>
<summary>Define graph nodes.</summary>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
There are two main nodes we need:
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
<details>
<summary>Compile the graph.</summary>
4. <details>
<summary>Define entry point and graph edges.</summary>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/v0.2/docs/concepts/#runnable-interface">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
First, we need to set the entry point for graph execution - `agent` node.
<details>
<summary>Execute the graph.</summary>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
- Conditional edge: after the agent is called, we should either:
- a. Run tools if the agent said to take an action, OR
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation

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