Compare commits

...
Author SHA1 Message Date
Vadym BardaandGitHub 016a9c1936 checkpoint-postgres: release 2.0.3 (#2455) 2024-11-18 16:55:54 -05:00
Nuno CamposandGitHub a2d6837fba Merge pull request #2413 from langchain-ai/vb/fix-pipeline
checkpoint-postgres: handle cases when conn.pipeline is not supported
2024-11-18 10:39:56 -08:00
Andrew NguonlyandGitHub f5bb2a3b04 docs: Update LangGraph Server API docs (#2451) 2024-11-18 09:38:38 -08:00
vbarda f807b73092 use capabilities 2024-11-18 12:15:18 -05:00
Nuno CamposandGitHub 167405daf2 Merge pull request #2434 from langchain-ai/nc/15nov/update-state-copy-parent
lib: When copying checkpoint, make it a child of the parent
2024-11-18 08:34:34 -08:00
William FHandGitHub c6360e5408 [Checkpoint] 2.0.5 (#2450) 2024-11-18 08:19:17 -08:00
vbarda f0505155a2 cache 2024-11-18 11:12:21 -05:00
886df0fa86 checkpoint: Add option to use persistent dict for in-memory checkpointer (#2439)
- This should only be used in very specific circunstances, sqlite or
postgres adapters much more appropriate in most circunstances

---------

Co-authored-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2024-11-18 16:04:05 +00:00
Kevin MarkhamandGitHub 3f1792d6ba docs: fix typo (#2406) 2024-11-18 09:11:57 -05:00
ZapironandGitHub 9208052a94 docs: Update link to LCEL Concept Guide (#2438)
Updated the link to the LCEL concept guide
2024-11-18 09:09:12 -05:00
Nuno Campos 7c11325e23 Separate 2024-11-15 17:33:01 -08:00
Nuno Campos d99dc7d81b Fix missing writes 2024-11-15 17:23:00 -08:00
Nuno Campos 973ad76a58 Fix 2024-11-15 17:05:18 -08:00
Nuno Campos 36e49eb190 Add distinct source 2024-11-15 17:04:40 -08:00
Nuno Campos 66b9a7dee7 lib: When copying checkpoint, make it a child of the parent 2024-11-15 16:56:17 -08:00
Nuno Campos 5494855ffa 0.2.50 2024-11-15 15:43:37 -08:00
Nuno Campos 38d93a324c 0.2.49 2024-11-15 15:02:31 -08:00
Nuno CamposandGitHub 07c65321c1 Merge pull request #2432 from langchain-ai/nc/15nov/copy-checkpoint
lib: Restore prev behavior for update_state(None)
2024-11-15 15:00:18 -08:00
Nuno Campos 1dbdd7df2e Lint 2024-11-15 14:54:55 -08:00
Nuno Campos dab29ce094 lib: Restore prev behavior for update_state(None)
- update_state(None) copies checkpoint and keeps current (PUSH) tasks, eg for replay
- update_state(None, as_node=END) clears all tasks (PUSH or PULL)
2024-11-15 14:48:56 -08:00
Vadym BardaandGitHub 0388534b9f docs: update double texting how-tos (#2431) 2024-11-15 22:45:36 +00:00
Nuno CamposandGitHub 81077e7c3a Merge pull request #2429 from langchain-ai/nc/15nov/sdk-js-types
sdk-js: Update types for state.task
2024-11-15 11:54:59 -08:00
Nuno Campos 29a0042149 sdk-js: Update types for state.task 2024-11-15 11:54:00 -08:00
William FHandGitHub e9162e2516 Update CLI pyproject.toml (#2428) 2024-11-15 11:00:32 -08:00
Vadym BardaandGitHub 0f6c001c25 docs: update replay in persistence concepts (#2427) 2024-11-15 18:49:20 +00:00
Eugene YurtsevandGitHub 7f26325c87 cli: minor wording change in new command (#2422) 2024-11-15 03:13:53 +00:00
Nuno CamposandGitHub 3c4ce3f945 Merge pull request #2420 from langchain-ai/nc/14nov/js-sdk-command
Nc/14nov/js sdk command
2024-11-14 18:21:38 -08:00
Nuno Campos 84ef939bf4 sdk-js 0.0.24 2024-11-14 18:17:22 -08:00
Nuno Campos bdc22ea127 sdk-js: Accept command when creating run 2024-11-14 18:17:04 -08:00
Vadym BardaandGitHub 5abbb79e1b Merge branch 'main' into vb/fix-pipeline 2024-11-14 19:08:18 -05:00
vbarda 0a5220aa07 code review 2024-11-14 19:06:41 -05:00
Nuno CamposandGitHub 970e68edcc Merge pull request #2417 from langchain-ai/vb/fix-debug-async
langgraph: add debug to AsyncPregelLoop
2024-11-14 06:53:18 -08:00
vbarda da1a80e86d lint 2024-11-14 09:37:39 -05:00
vbarda c4b240e0c2 langgraph: add debug to AsyncPregelLoop 2024-11-14 09:36:33 -05:00
vbarda c2052d11c2 checkpoint-postgres: remove pipeline flag in cursor 2024-11-13 21:42:51 -05:00
Vadym BardaandGitHub dc0281b99c docs: update rollback in double-texting concepts (#2412) 2024-11-13 21:21:12 -05:00
Nuno Campos 3a860ad537 0.2.48 2024-11-13 17:45:04 -08:00
Nuno Campos 7051bccc30 checkpoint 2.0.4 2024-11-13 17:37:01 -08:00
Nuno Campos 199e41b228 sdk py 0.1.36 2024-11-13 17:07:07 -08:00
Nuno Campos 229a9e19a8 sdk-py: Add command arg for creating runs 2024-11-13 17:06:56 -08:00
Nuno Campos 3c3a1a1f35 0.2.47 2024-11-13 14:02:25 -08:00
Nuno CamposandGitHub f11127648e Merge pull request #2346 from langchain-ai/nc/4nov/send-eager
lib: Execute Sends in the superstep that originated them (feature-flagged)
2024-11-13 13:39:50 -08:00
Nuno CamposandGitHub 29f833b1a7 Merge pull request #2393 from langchain-ai/nc/11nov/command-resume
lib: Add interrupt() function
2024-11-13 13:34:19 -08:00
Nuno CamposandGitHub 7a3ea42743 Merge pull request #2410 from langchain-ai/nc/13nov/command-dataclass
Nc/13nov/command dataclass
2024-11-13 13:28:13 -08:00
Nuno Campos a94902db8a Lint 2024-11-13 13:17:02 -08:00
Nuno Campos 9fd152ef3a format 2024-11-13 13:14:03 -08:00
Nuno Campos 03bc9ba6e6 Make Command a dataclass 2024-11-13 13:11:28 -08:00
Nuno Campos 7fe6f88876 Add resumeable/ns properties to Interrupt 2024-11-13 12:51:55 -08:00
Nuno CamposandGitHub 1d88affd29 Merge pull request #2400 from langchain-ai/nc/12nov/command
Make Command accept generic arg for destinations
2024-11-13 10:03:27 -08:00
Eugene YurtsevandGitHub 6906e12edb cli: add ability to output docker compose file (#2379)
* Add ability to output docker compose file `langgraph dockerfile
Dockerfile --add-docker-compose`
* Add emoji in places
2024-11-13 12:42:58 -05:00
William FHandGitHub 7b4c29a20d [Checkpointers] MemorySaver: refrain from overwriting writes (#2399) 2024-11-13 00:20:38 +00:00
Nuno Campos 16bfa80b58 Make Command accept generic arg for destinations 2024-11-12 16:18:23 -08:00
Eugene YurtsevandGitHub cb10437c3f cli: Add default to interactive flow in new command (#2398) 2024-11-12 16:38:15 -05:00
Eugene YurtsevandGitHub 2bcf1c0a20 cli[minor]: Add langgraph new command (#2369)
Adds a "new" command to create langgraph application from a template.
2024-11-12 14:30:51 -05:00
Nuno Campos 00964b18f6 Undo 2024-11-11 18:12:25 -08:00
Nuno Campos 0d5c6201d3 Disable in py 3.10 or below for async 2024-11-11 18:09:58 -08:00
Nuno Campos 86d2847dab Use neg idx 2024-11-11 17:57:45 -08:00
Nuno Campos b3a4eaa967 Remove print 2024-11-11 17:56:10 -08:00
Nuno Campos 87fc519ce7 Remove print 2024-11-11 17:54:56 -08:00
Nuno Campos ef3a1ee997 Undo 2024-11-11 17:54:04 -08:00
Nuno Campos 311e16dffd Remove prints 2024-11-11 17:52:18 -08:00
Nuno Campos c83b8f6d04 Update 2024-11-11 17:49:58 -08:00
Nuno Campos 62d3a85b07 Add sync test 2024-11-11 17:46:54 -08:00
Nuno Campos 810ae0ef51 lib: Add interrupt() function
- This works similarly to the input() function from stdlib
- calling it in a node interrupts execution
- invoking the graph with Command(resume=...) will set ... as the return value of interrupt() so that the node can access the "answer" to the "question"
- This PR also starts the work to control the graph on invoke/stream with Command() input, to be continued in a future PR
2024-11-11 17:44:03 -08:00
Nuno Campos 2ff49d2200 Update 2024-11-11 15:43:20 -08:00
Nuno Campos d0567dc7be Add feature flag (default off) so we can merge this before releasing
- Add additional ci job to test with FF on
2024-11-11 15:38:48 -08:00
Nuno Campos ea64ac5c07 Update 2024-11-11 14:18:14 -08:00
Nuno Campos 090b53ccc1 Lint 2024-11-11 14:14:23 -08:00
Nuno Campos 0e872e7482 Lin t 2024-11-11 14:12:33 -08:00
Nuno Campos 3ad966e057 Update 2024-11-11 14:08:59 -08:00
Nuno CamposandGitHub a73f9affab Merge pull request #2391 from langchain-ai/nc/11nov/control-to-command
lib: Rename Control to GraphCommand
2024-11-11 14:06:17 -08:00
Nuno Campos 89a0859928 Execute Sends in same super step that triggered them
- Keep old code path for compatibility with existing checkpoints
- Keep a similar order of application of updates, in some cases there will be no visible change
- Update task path for Sends to contain the path of all the parent tasks (multiple parents when a Send task creates another Send)
- That lineage path is used to ensure order of application of updates respects their logical lineage (ie updates from parents always applied before their child tasks)
- Move Interrupt writes to use negative indexes, which allow replacing/shadowing (when task is re-run it may interrupt again, or succeed)
- Runner will now attempt to schedule new Send tasks as soon as the write is received (ie while the originating node is still running)
- Update kafka scheduler to support new Send behavior
2024-11-11 14:01:16 -08:00
Nuno Campos 75c502cd93 Lint 2024-11-11 13:59:18 -08:00
Nuno Campos 0cc45f7a35 Lint 2024-11-11 13:04:23 -08:00
Nuno Campos efb1dd6a10 lib: Rename Control to GraphCommand 2024-11-11 13:01:04 -08:00
Nuno CamposandGitHub 6edb213fe7 Merge pull request #2388 from langchain-ai/nc/11nov/update-none-clear-all-tasks
lib: update_state(values=None) should clear all tasks
2024-11-11 12:07:51 -08:00
Nuno Campos c0513076a2 Lint 2024-11-11 11:07:36 -08:00
Nuno Campos c043f148a6 lib: update_state(values=None) should clear all tasks 2024-11-11 10:48:34 -08:00
David DuongandGitHub 366b5e04c7 Merge pull request #2376 from langchain-ai/dqbd/js-0.0.23-lc_build
fix(sdk-js): move to `@langchain/scripts` for building, bump to 0.0.23
2024-11-11 15:33:53 +01:00
Tat Dat Duong 72239d2228 Bump to 0.0.23, use @langchain/scripts 0.1.4 2024-11-11 15:01:12 +01:00
Vadym BardaandGitHub b7f238975a docs: fix nav sidebar (#2382) 2024-11-10 16:32:14 -05:00
Tat Dat Duong a3c45141ee Make sure we actually build the CJS 2024-11-08 20:52:44 +01:00
Tat Dat Duong f7b899a54d fix(sdk-js): move to @langchain/scripts for building 2024-11-08 20:08:38 +01:00
Vadym BardaandGitHub 95477277a2 docs: improve breadcrumbs behavior (#2375) 2024-11-08 18:39:59 +00:00
Andrew NguonlyandGitHub 64b446f671 Update LangGraph server API docs (#2374) 2024-11-08 10:32:17 -08:00
David DuongandGitHub c2d2e44794 Merge pull request #2372 from langchain-ai/dqbd/0.0.22-sdk-js
feat(sdk-js): bump to 0.0.22
2024-11-08 16:17:45 +01:00
Eugene YurtsevandGitHub f60b9f3b43 cli: add --version option (#2373)
- add version option
- add unit test
2024-11-08 02:49:30 +00:00
Tat Dat Duong e3acfa3435 feat(sdk-js): bump to 0.0.22 2024-11-08 03:19:27 +01:00
Vadym BardaandGitHub 0f92c89529 docs: show nav menu for mobile (#2371) 2024-11-07 20:31:49 -05:00
Andrew NguonlyandGitHub 7cba75ec35 sdk-js: Add custom and messages-tuple stream modes (#2370) 2024-11-07 16:32:08 -08:00
Vadym BardaandGitHub 8408ba3a3e docs: add breadcrumbs (#2363) 2024-11-07 21:33:55 +00:00
Nuno CamposandGitHub c8c58b30d5 Merge pull request #2368 from langchain-ai/nc/7nov/control
lib: Rename args in Control object
2024-11-07 13:19:12 -08:00
Nuno Campos b4bed3329c lib: Rename args in Control object
- update_state -> state
- trigger -> goto
2024-11-07 13:12:19 -08:00
Eugene YurtsevandGitHub 4eec3aa69e docs: fix some typos (#2364) 2024-11-07 14:52:47 -05:00
Vadym BardaandGitHub 35e3276e34 langgraph: add add_sequence to StateGraph (#2352) 2024-11-07 13:16:34 -05:00
Nuno CamposandGitHub b346f4ead8 Merge pull request #2358 from langchain-ai/nc/6nov/kafka-missing-task-id
kafka: Add missing task id for TaskNotFound error
2024-11-06 16:48:25 -08:00
Vadym BardaandGitHub fef748e6cc docs: add another ignore url pattern to link check (#2357) 2024-11-07 00:43:22 +00:00
Nuno Campos 19cc95f7b6 kafka: Add missing task id for TaskNotFound error 2024-11-06 16:41:43 -08:00
Vadym BardaandGitHub eef65d94ac docs: fix numbered list in assistant versioning (#2356) 2024-11-06 22:17:01 +00:00
Nuno Campos 72d497e052 Move code 2024-11-06 09:02:06 -08:00
Nuno CamposandGitHub 32f58258aa Merge pull request #2355 from langchain-ai/nc/6nov/loop-match-writes
lib: Split out _match_writes util in PregelLoop
2024-11-06 08:55:10 -08:00
Nuno Campos 87d57b434a lib: Split out _match_writes util in PregelLoop 2024-11-06 08:48:30 -08:00
Nuno CamposandGitHub 4bbbb7d246 Merge pull request #2354 from langchain-ai/nc/6nov/cached-tasks-output-timing
lib: For cached tasks, emit output events after task events
2024-11-06 08:29:38 -08:00
Nuno Campos c1ce3c6b5f lib: For cached tasks, emit output events after task events 2024-11-06 08:22:54 -08:00
Nuno CamposandGitHub 511de6f5f6 Merge pull request #2353 from langchain-ai/dqbd/runnable-passthrough-test
fix(graph): invalid graph representation if RunnablePassthrough is used
2024-11-06 08:14:19 -08:00
Nuno Campos 4dc08cd724 Fix 2024-11-06 08:07:48 -08:00
Tat Dat Duong d7b9b3b01d fix(graph): invalid graph representation if RunnablePassthrough is used 2024-11-06 16:34:29 +01:00
Vadym BardaandGitHub a82ded65c6 docs: fix install/env cell for customer support tutorial (#2350) 2024-11-05 19:50:05 -05:00
Brace SproulandGitHub 1ca49fa568 Merge pull request #2348 from langchain-ai/brace/filter-status-js
feat(js-sdk): Add status field in search args
2024-11-05 16:40:23 -08:00
bracesproul 4eb3cd32fa cr 2024-11-05 16:04:52 -08:00
Brace SproulandGitHub 80d2a315aa Merge branch 'main' into brace/filter-status-js 2024-11-05 16:03:58 -08:00
bracesproul 82c9aa8485 feat(js-sdk): Add status field in search args 2024-11-05 16:03:24 -08:00
Nuno CamposandGitHub 8ecfafefbf Merge pull request #2347 from langchain-ai/nc/5nov/control-serializable
lib: Make Control object serializable
2024-11-05 15:34:13 -08:00
Nuno Campos 010564cbb3 lib: Make Control object serializable 2024-11-05 15:24:20 -08:00
Nuno Campos 18a3fa4a00 Ignore unknown tasks 2024-11-05 14:22:23 -08:00
Nuno Campos de8e487ff1 Add todo 2024-11-05 10:53:42 -08:00
Nuno CamposandGitHub e5ebdff4d8 Merge pull request #2342 from langchain-ai/nc/5nov/send-test-interrupt-before
Add two more test cases for Send + interrupt
2024-11-05 09:34:04 -08:00
Nuno Campos f283dac325 Add one more test for send-react-interrupt flow with replacing tool call 2024-11-05 09:27:02 -08:00
Nuno Campos 639501809c api: Add one more test case for send + interrupt before
- Testing same exact behavior as send + interrupt after
2024-11-05 09:15:46 -08:00
Nuno CamposandGitHub 36e6b89081 Merge pull request #2333 from langchain-ai/nc/apply-writes-order
lib: Enforce write application order in apply_writes
2024-11-05 09:07:23 -08:00
Nuno CamposandGitHub 90639e6cd7 Merge pull request #2332 from langchain-ai/nc/4nov/update-state-latest
lib: When updating state from latest, apply pending writes first
2024-11-04 16:59:51 -08:00
Nuno Campos dec0b7f439 Lint 2024-11-04 16:43:07 -08:00
Nuno Campos 04657408f8 lib: Enforce write application order in apply_writes
- Previously order was enforced in prepare_next_tasks, but that's not a good fit for future features
- This changes order between PULL and PUSH tasks, updates from PUSH tasks will now be applied after updates from PULL tasks
2024-11-04 16:39:04 -08:00
Nuno Campos 971d746061 Lint 2024-11-04 16:33:20 -08:00
Nuno Campos 9a2ba8c8cd lib: When updating state from latest, apply pending writes first
- This picks a default value for as_node which matches the node which last acted, even if the step didnt finish (due to an interrupt)
2024-11-04 16:20:21 -08:00
Nuno CamposandGitHub b50d41bbf3 Merge pull request #2331 from langchain-ai/nc/4nov/get-state-latest-next
lib: When getting latest state, alst make `next` reflect pending writes
2024-11-04 16:16:53 -08:00
Nuno Campos b71fd5092b lib: When getting latest state, alst make next reflect pending writes
- ie. tasks already executed should not show up in `next` list
2024-11-04 16:03:34 -08:00
Nuno CamposandGitHub f9b151b67f Merge pull request #2330 from langchain-ai/nc/4nov/test-react-send
lib: Add test for react architecture using Send + interrupt_before
2024-11-04 15:58:27 -08:00
William FHandGitHub 7ba9a66301 [Docs] Clarify checkpointer options (#2328) 2024-11-04 15:43:00 -08:00
Nuno CamposandGitHub 1b85764bf6 Merge pull request #2329 from langchain-ai/nc/4nov/get-state-apply-pending-writes
lib: In calls to get_state apply pending writes
2024-11-04 15:42:23 -08:00
Nuno CamposandGitHub 5e4c928948 Merge pull request #2327 from langchain-ai/nc/4nov/send-tests
lib: Add two more tests for Send
2024-11-04 15:42:08 -08:00
Nuno Campos ae282e3ae1 lib: Add test for react architecture using Send + interrupt_before
- Both for cond edge and edgeless graphs
2024-11-04 15:41:02 -08:00
Nuno Campos de3b654735 Add one more assertion 2024-11-04 15:14:29 -08:00
Nuno Campos d28734f287 Lint 2024-11-04 15:12:34 -08:00
Nuno Campos f9409022ed lib: In calls to get_state apply pending writes
- When calling get_state without a checkpoint id (ie to get the latest state) apply any pending writes for current checkpoint
2024-11-04 15:10:12 -08:00
Nuno Campos 8138c88b41 Add async versions 2024-11-04 11:56:31 -08:00
Nuno Campos 38332fd3c6 Lint 2024-11-04 11:55:14 -08:00
Nuno Campos 5606bef3dd lib: Add two more tests for Send 2024-11-04 11:52:39 -08:00
Vadym BardaandGitHub 895079bbdc langgraph: release 0.2.45 (#2326) 2024-11-04 14:34:07 -05:00
Nuno CamposandGitHub 6aaf80f2fd Merge pull request #2325 from langchain-ai/nc/4nov/unset-skip-done-tasks
lib: Unset skip_done_tasks after each tick of the loop
2024-11-04 11:30:08 -08:00
Vadym BardaandGitHub 2e656d9145 langgraph: add config metadata to pregel loop (#2323) 2024-11-04 19:26:34 +00:00
Nuno CamposandGitHub 1fb8e013f7 Merge pull request #2303 from langchain-ai/nc/1nov/test-send-order
Test order of update application after Send
2024-11-04 11:26:04 -08:00
Nuno CamposandGitHub f8fe2041d9 Merge pull request #2144 from langchain-ai/nc/19oct/graph-control
lib: Add support for graphs without edges
2024-11-04 11:25:40 -08:00
Nuno Campos 014f8485a6 lib: Unset skip_done_tasks after each tick of the loop 2024-11-04 11:23:53 -08:00
David DuongandGitHub 58d7eb9b17 Merge pull request #2322 from langchain-ai/dqbd/cli-prebuild-js
feat(cli): add JS prebuild script
2024-11-04 18:13:25 +01:00
Tat Dat Duong 44ff5f1a5a Update tests 2024-11-04 14:00:02 +01:00
David DuongandGitHub 46f25cf926 Merge pull request #2307 from langchain-ai/dqbd/cli-js-other-pkg-managers
feat(cli): add support for other JS package managers based off package lock
2024-11-04 13:59:28 +01:00
Tat Dat Duong e1467c27cb Remove newline 2024-11-04 13:52:40 +01:00
Tat Dat Duong 5deedfe548 Bump to 0.1.53 2024-11-04 13:51:46 +01:00
Tat Dat Duong 30937d0406 feat(cli): add JS prebuild script 2024-11-04 13:50:41 +01:00
David DuongandGitHub 6eb8130419 Merge pull request #2310 from langchain-ai/dqbd/sdk-js-types
feat(sdk-js): improve types for drawable graph, interrupts and metadata
2024-11-04 13:26:58 +01:00
Tat Dat Duong 8455771eb9 Bump to 0.0.21 2024-11-04 13:20:36 +01:00
Tat Dat Duong 39dcafcdea feat(sdk-js): improve types for drawable graph, interrupts and metadata 2024-11-02 02:48:47 +01:00
Tat Dat Duong 7854a19dac Update test 2024-11-02 02:05:42 +01:00
AllenandGitHub 18cea39090 fix(docs): function call in partial (#2297)
Docs use prompts with partial + function call, which would result in a
static datetime as opposed to a dynamic datetime being rendered on use.
2024-11-02 00:37:16 +00:00
Tat Dat Duong bf3098a075 feat(cli): add support for other JS package managers based off package lock 2024-11-02 00:44:08 +01:00
Nuno Campos 1e3953d1e0 Test order of update application after Send
- updates from inside Send tasks are applied in the order the Sends were created, if when you fan out, and have each task write results to a list with reducer, the final list is in the order you used when triggering
2024-11-01 13:23:10 -07:00
Nuno Campos 18e71469e1 Lint 2024-10-31 12:57:00 -07:00
Nuno Campos 2624fc43dd Fix 2024-10-31 09:40:24 -07:00
Nuno Campos 0c5c2e6370 lib: Add support for graphs without edges
- Return Control(update_state=, trigger=, send=) from your nodes instead
- Annotate nodes with Control[Literal["destination"]] to see your graph connections drawn
2024-10-31 09:38:43 -07:00
97 changed files with 9241 additions and 1688 deletions
+8 -1
View File
@@ -19,14 +19,19 @@ jobs:
- "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 }})"
name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }}, ff-send-v2: ${{ matrix.ff-send-v2 }})"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
@@ -52,6 +57,8 @@ jobs:
- name: Run tests
shell: bash
env:
LANGGRAPH_FF_SEND_V2: ${{ matrix.ff-send-v2 }}
run: |
make test
+12 -3
View File
@@ -44,6 +44,8 @@ jobs:
deploy:
# needs: run-changed-notebooks
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
@@ -58,8 +60,14 @@ jobs:
- name: Install dependencies
run: |
poetry install --with test
poetry run pip install -U pytest pytest-check-links langsmith langchain GitPython
poetry install --with test --no-root
poetry run pip install -U \
pytest \
pytest-check-links \
langsmith \
langchain \
GitPython \
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
- name: Lint Docs
# This step lints the docs using the existing linting set up.
@@ -82,7 +90,8 @@ jobs:
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai.com/index/memory-and-new-controls-for-chatgpt/" \
--check-links-ignore "https://openai\.com/.*" \
--check-links-ignore "https://pepy\.tech/.*" \
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
else
+12 -12
View File
@@ -86,19 +86,19 @@ To create an assistant using the studio do the following steps:
1. Click on the "Create New Assistant" button:
![click create](./img/click_create_assistant.png)
![click create](./img/click_create_assistant.png)
2. Use the create assistant pane to enter info for the assistant you wish to create, and then click create:
1. Use the create assistant pane to enter info for the assistant you wish to create, and then click create:
![create](./img/create_assistant.png)
![create](./img/create_assistant.png)
3. See that your assistant was created and is displayed in the Studio
1. See that your assistant was created and is displayed in the Studio
![view create](./img/create_assistant_view.png)
![view create](./img/create_assistant_view.png)
4. Click on the edit button next to the selected assistant to manage your created assistant:
1. Click on the edit button next to the selected assistant to manage your created assistant:
![create edit](./img/edit_created_assistant.png)
![create edit](./img/edit_created_assistant.png)
## Create a new version for your assistant
@@ -131,15 +131,15 @@ Let's now say we wanted to add a system prompt to our assistant. We can do this
1. First, click on the edit button next to the `openai_assistant`. Then, add a system prompt and click "Save New Version":
![create new version](./img/create_new_version.png)
![create new version](./img/create_new_version.png)
2. Then you can see it is selected in the assistant dropdown:
1. Then you can see it is selected in the assistant dropdown:
![see version dropdown](./img/see_new_version.png)
![see version dropdown](./img/see_new_version.png)
3. And you can see all the version history in the edit pane for the assistant:
1. And you can see all the version history in the edit pane for the assistant:
![see versions](./img/see_version_history.png)
![see versions](./img/see_version_history.png)
## Point your assistant to a different version
@@ -94,6 +94,7 @@ Now we can start our two runs and join the second on euntil it has completed:
assistant_id,
input={"messages": [{"role": "user", "content": "what's the weather in sf?"}]},
)
# sleep a bit to get partial outputs from the first run
await asyncio.sleep(2)
run = await client.runs.create(
thread["thread_id"],
@@ -114,6 +115,7 @@ Now we can start our two runs and join the second on euntil it has completed:
assistantId,
{ input: { messages: [{ role: "human", content: "what's the weather in sf?" }] } }
);
// sleep a bit to get partial outputs from the first run
await new Promise(resolve => setTimeout(resolve, 2000));
let run = await client.runs.create(
@@ -95,7 +95,6 @@ Now let's run a thread with the multitask parameter set to "rollback":
assistant_id,
input={"messages": [{"role": "user", "content": "what's the weather in sf?"}]},
)
await asyncio.sleep(2)
run = await client.runs.create(
thread["thread_id"],
assistant_id,
@@ -115,7 +114,6 @@ Now let's run a thread with the multitask parameter set to "rollback":
assistantId,
{ input: { messages: [{ role: "human", content: "what's the weather in sf?" }] } }
);
await new Promise(resolve => setTimeout(resolve, 2000));
let run = await client.runs.create(
thread["thread_id"],
@@ -139,7 +137,7 @@ Now let's run a thread with the multitask parameter set to "rollback":
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
}" && sleep 2 && curl --request POST \
}" && curl --request POST \
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
--header 'Content-Type: application/json' \
--data "{
+1 -1
View File
@@ -1,4 +1,4 @@
# Quick Start
# LangGraph Cloud Quick Start
In this tutorial you will build and deploy a simple chatbot agent that can look things up on the internet. You will be using [LangGraph Cloud](../concepts/langgraph_cloud.md), [LangGraph Studio](../concepts/langgraph_studio.md) to visualize and test it out, and [LangGraph SDK](./reference/sdk/python_sdk_ref.md) to interact with the deployed agent.
+114 -13
View File
@@ -60,6 +60,16 @@
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"409": {
"description": "Conflict",
"content": {
@@ -116,6 +126,16 @@
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
@@ -2848,9 +2868,18 @@
"description": "The cron schedule to execute this job on."
},
"assistant_id": {
"type": "string",
"format": "uuid",
"title": "Assistant Id"
"anyOf": [
{
"type": "string",
"format": "uuid",
"title": "Assistant Id"
},
{
"type": "string",
"title": "Graph Id"
}
],
"description": "The assistant ID or graph name to run. If using graph name, will default to the assistant automatically created from that graph by the server."
},
"input": {
"anyOf": [
@@ -3151,6 +3180,66 @@
],
"title": "Run"
},
"Send": {
"type": "object",
"title": "Send",
"description": "A message to send to a node.",
"properties": {
"node": {
"type": "string",
"title": "Node",
"description": "The node to send the message to."
},
"input": {
"type": "object",
"title": "Message",
"description": "The message to send."
}
},
"required": [
"node",
"input"
]
},
"Command": {
"type": "object",
"title": "Command",
"description": "The command to run.",
"properties": {
"update": {
"type": "object",
"title": "Update",
"description": "An update to the state."
},
"resume": {
"type": [
"object",
"array",
"number",
"string",
"null"
],
"title": "Resume",
"description": "A value to pass to an interrupted node."
},
"send": {
"anyOf": [
{
"$ref": "#/components/schemas/Send"
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/Send"
}
},
{
"type": "null"
}
]
}
}
},
"RunCreateStateful": {
"properties": {
"assistant_id": {
@@ -3176,13 +3265,19 @@
"input": {
"anyOf": [
{
"items": {
"type": "object"
},
"type": "array"
"type": "object"
},
{
"type": "object"
"type": "null"
}
],
"title": "Input",
"description": "The input to the graph."
},
"command": {
"anyOf": [
{
"$ref": "#/components/schemas/Command"
},
{
"type": "null"
@@ -3385,13 +3480,19 @@
"input": {
"anyOf": [
{
"items": {
"type": "object"
},
"type": "array"
"type": "object"
},
{
"type": "object"
"type": "null"
}
],
"title": "Input",
"description": "The input to the graph."
},
"command": {
"anyOf": [
{
"$ref": "#/components/schemas/Command"
},
{
"type": "null"
+4 -4
View File
@@ -53,7 +53,7 @@ Youll build a Docker image using the [LangGraph CLI](./langgraph_cli.md), whi
For more information, please see:
* [Self-Hosted conceptual guide](./self_hosted.md)
* [Self-Hosted Deployment how-to guide](../how-tos/deploy-self-hosted.md)
* [Self-Hosted deployment how-to guide](../how-tos/deploy-self-hosted.md)
## Cloud SaaS
@@ -89,8 +89,8 @@ For more information please see:
## Related
For more information please see:
For more information, please see:
* [LangGraph Platform Plans](./plans.md)
* [LangGraph Platform Pricing](https://www.langchain.com/langgraph-platform-pricing)
* [LangGraph Platform plans](./plans.md)
* [LangGraph Platform pricing](https://www.langchain.com/langgraph-platform-pricing)
* [Deployment how-to guides](../how-tos/index.md#deployment)
+3 -5
View File
@@ -15,10 +15,12 @@ If you do not want to use LangGraph Platform, we describe the options we have im
![](img/double_texting.png)
## Reject
This is the simplest option, this just rejects any follow up runs and does not allow double texting.
See the [how-to guide](../cloud/how-tos/reject_concurrent.md) for configuring the reject double text option.
## Enqueue
This is a relatively simple option which continues the first run until it completes the whole run, then sends the new input as a separate run.
See the [how-to guide](../cloud/how-tos/enqueue_concurrent.md) for configuring the enqueue double text option.
@@ -35,10 +37,6 @@ See the [how-to guide](../cloud/how-tos/interrupt_concurrent.md) for configuring
## Rollback
This option rolls back all work done up until that point.
It then sends the user input in, basically as if it just followed the original run input.
This may create some weird states - for example, you may have two `User` messages in a row, with no `Asssitant` message in between them.
You will need to make sure the LLM you are calling can handle that, or combine those into a single `User` message.
This option interrupts the current execution AND rolls back all work done up until that point, including the original run input. It then sends the new user input in, basically as if it was the original input.
See the [how-to guide](../cloud/how-tos/rollback_concurrent.md) for configuring the rollback double text option.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+4 -4
View File
@@ -42,14 +42,14 @@ The LangGraph Platform offers a few different deployment options described in th
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
* You can always deploy LangGraph applications on your own infrastructure using the open-source LangGraph project without using LangGraph Platform.
**High Level**
### High Level
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
**Components**
### Components
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
@@ -59,7 +59,7 @@ The LangGraph Platform comprises several components that work together to suppor
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
- [Remote Graph](../how-tos/use-remote-graph.md): A RemoteGraph allows you to interact with any deployed LangGraph application as though it were running locally.
**LangGraph Server**
### LangGraph Server
- [Application Structure](./application_structure.md): A LangGraph application consists of one or more graphs, a LangGraph API Configuration file (`langgraph.json`), a file that specifies dependencies, and environment variables.
- [Assistants](./assistants.md): Assistants are a way to save and manage different configurations of your LangGraph applications.
@@ -67,7 +67,7 @@ The LangGraph Platform comprises several components that work together to suppor
- [Cron Jobs](./langgraph_server.md#cron-jobs): Cron jobs are a way to schedule tasks to run at specific times in your LangGraph application.
- [Double Texting](./double_texting.md): Double texting is a common issue in LLM applications where users may send multiple messages before the graph has finished running. This guide explains how to handle double texting with LangGraph Deploy.
**Deployment Options**
### Deployment Options
- [Self-Hosted Lite](./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
+4 -4
View File
@@ -1,12 +1,12 @@
# Cloud SaaS
!!! info "Prerequisites"
- [LangGraph Platform](./langgraph_platform.md)
!!! info "Prerequisites"
- [LangGraph Platform](./langgraph_platform.md)
- [LangGraph Server](./langgraph_server.md)
## Overview
LangGraph's Cloud SaaS is a managed service that provides a scalable and secure environment for deploying LangGraph APIs. It is designed to work seamlessly with your LangGraph API regardless of how it is defined, what tools it uses, or any dependencies. Cloud SaaS provides a simple way to deploy and manage your LangGraph API in the cloud.
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.
## Deployment
@@ -35,4 +35,4 @@ A high-level diagram of a Cloud SaaS deployment.
## Related
- [Deployment Options](./deployment_options.md)
- [Deployment Options](./deployment_options.md)
+1 -1
View File
@@ -391,7 +391,7 @@ Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-li
It can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you ["compile" a graph](#compiling-your-graph). You can set breakpoints either _before_ a node executes (using `interrupt_before`) or after a node executes (using `interrupt_after`.)
You **MUST** use a [checkpoiner](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
You **MUST** use a [checkpointer](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
In order to resume execution, you can just invoke your graph with `None` as the input.
+1 -1
View File
@@ -159,7 +159,7 @@ You must pass these when invoking the graph as part of the `configurable` portio
# {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} # also valid config
config = {"configurable": {"thread_id": "1"}}
graph.invoke(inputs, config=config)
graph.invoke(None, config=config)
```
Importantly, LangGraph knows whether a particular checkpoint has been executed previously. If it has, LangGraph simply *re-plays* that particular step in the graph and does not re-execute the step. See this [how to guide on time-travel to learn more about replaying](../how-tos/human_in_the_loop/time-travel.ipynb).
+6 -2
View File
@@ -29,7 +29,11 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
## How it works
- Deploy Redis and Postgres instances on your own infrastructure.
- Build the docker image for [LangGraph Server](./langgraph_server.md) using the [LangGraph CLI](./langgraph_cli.md)
- Build the docker image for [LangGraph Server](./langgraph_server.md) using the [LangGraph CLI](./langgraph_cli.md).
- Deploy a web server that will run the docker image and pass in the necessary environment variables.
See the [how-to guide](../how-tos/deploy-self-hosted.md)
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
## Related
- [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
+1 -2
View File
@@ -40,7 +40,7 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
- [How to delete messages](memory/delete-messages.ipynb)
- [How to add summary conversation memory](memory/add-summary-conversation-history.ipynb)
### Human in the Loop
### Human-in-the-loop
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
@@ -99,7 +99,6 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to run graph asynchronously](async.ipynb)
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to use a Pydantic model as your state](state-model.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 pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
+7
View File
@@ -3,6 +3,13 @@ title: Reference
description: API reference for LangGraph
---
<style>
.md-sidebar {
display: block !important;
}
</style>
# Reference
Welcome to the LangGraph API reference! This reference provides detailed information about the LangGraph API, including classes, methods, and other components.
@@ -102,7 +102,7 @@
"from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n",
"\n",
"# LCEL docs\n",
"url = \"https://python.langchain.com/docs/concepts/#langchain-expression-language-lcel\"\n",
"url = \"https://python.langchain.com/docs/concepts/lcel/\"\n",
"loader = RecursiveUrlLoader(\n",
" url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n",
")\n",
@@ -35,7 +35,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas"
"%pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas openai"
]
},
{
@@ -55,6 +55,7 @@
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"_set_env(\"TAVILY_API_KEY\")"
]
},
@@ -85,7 +86,9 @@
"cell_type": "code",
"execution_count": 21,
"id": "71638c2a-5038-439e-907a-de2bb548db34",
"metadata": {"hide_from_vcr": true},
"metadata": {
"hide_from_vcr": true
},
"outputs": [],
"source": [
"import os\n",
@@ -176,7 +179,9 @@
"cell_type": "code",
"execution_count": 22,
"id": "654e2f81",
"metadata": {"hide_from_vcr": true},
"metadata": {
"hide_from_vcr": true
},
"outputs": [],
"source": [
"import re\n",
@@ -1077,7 +1082,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"part_1_tools = [\n",
" TavilySearchResults(max_results=1),\n",
@@ -1893,7 +1898,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"part_2_tools = [\n",
" TavilySearchResults(max_results=1),\n",
@@ -2472,7 +2477,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"\n",
"# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\n",
@@ -3183,7 +3188,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"update_flight_safe_tools = [search_flights]\n",
"update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\n",
@@ -3215,7 +3220,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"book_hotel_safe_tools = [search_hotels]\n",
"book_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\n",
@@ -3247,7 +3252,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"book_car_rental_safe_tools = [search_car_rentals]\n",
"book_car_rental_sensitive_tools = [\n",
@@ -3282,7 +3287,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"\n",
"book_excursion_safe_tools = [search_trip_recommendations]\n",
"book_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\n",
@@ -3389,7 +3394,7 @@
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
").partial(time=datetime.now())\n",
").partial(time=datetime.now)\n",
"primary_assistant_tools = [\n",
" TavilySearchResults(max_results=1),\n",
" search_flights,\n",
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "4a1aae78-88a6-4133-b905-7e46c8e3772f",
"metadata": {},
"source": [
"# Quick Start\n",
"# LangGraph Quick Start\n",
"\n",
"In this comprehensive quick start, we will build a support chatbot in LangGraph that can:\n",
"\n",
+239 -6
View File
@@ -25,6 +25,7 @@ theme:
- navigation.sections
- navigation.instant.prefetch
- navigation.instant.progress
- navigation.path
- navigation.prune
- navigation.tabs
- navigation.tabs.sticky
@@ -87,12 +88,242 @@ plugins:
filters:
- "!^_"
nav:
# Setting the names of the nav items explicitly due to mkdocs
# how-reload being a bit buggy with the names of the tabs.
- Home: "index.md"
- Tutorials: "tutorials/index.md"
- Concepts: "concepts/index.md"
- "How-to Guides": "how-tos/index.md"
- Home: index.md
- Tutorials:
- tutorials/index.md
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- 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
- 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
- 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
- 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/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
- 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/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
- 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
- Troubleshooting:
- Troubleshooting: how-tos#troubleshooting
- 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:
@@ -206,3 +437,5 @@ validation:
# because it creates anchors in the generated report
# and those anchors are not available in the actual doc
anchors: info
# this is needed to handle headers with anchors for nav
not_found: info
+11
View File
@@ -34,6 +34,17 @@
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;
}
@@ -3,7 +3,7 @@ from contextlib import contextmanager
from typing import Any, Iterator, Optional, Sequence, Union
from langchain_core.runnables import RunnableConfig
from psycopg import Connection, Cursor, Pipeline
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.errors import UndefinedTable
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
@@ -52,6 +52,7 @@ class PostgresSaver(BasePostgresSaver):
self.conn = conn
self.pipe = pipe
self.lock = threading.Lock()
self.supports_pipeline = Capabilities().has_pipeline()
@classmethod
@contextmanager
@@ -365,6 +366,13 @@ class PostgresSaver(BasePostgresSaver):
@contextmanager
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
"""Create a database cursor as a context manager.
Args:
pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
with _get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
@@ -379,10 +387,17 @@ class PostgresSaver(BasePostgresSaver):
elif pipeline:
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
with self.lock, conn.pipeline(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
if self.supports_pipeline:
with self.lock, conn.pipeline(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
# Use connection's transaction context manager when pipeline mode not supported
with self.lock, conn.transaction(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
@@ -3,7 +3,7 @@ from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Iterator, Optional, Sequence, Union
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.errors import UndefinedTable
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
@@ -55,6 +55,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
self.pipe = pipe
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
self.supports_pipeline = Capabilities().has_pipeline()
@classmethod
@asynccontextmanager
@@ -323,6 +324,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
async def _cursor(
self, *, pipeline: bool = False
) -> AsyncIterator[AsyncCursor[DictRow]]:
"""Create a database cursor as a context manager.
Args:
pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
async with _get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
@@ -337,10 +345,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
elif pipeline:
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
async with self.lock, conn.pipeline(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
if self.supports_pipeline:
async with self.lock, conn.pipeline(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
# Use connection's transaction context manager when pipeline mode not supported
async with self.lock, conn.transaction(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
async with self.lock, conn.cursor(
binary=True, row_factory=dict_row
@@ -84,7 +84,7 @@ select
and cw.checkpoint_id = checkpoints.checkpoint_id
) as pending_writes,
(
select array_agg(array[cw.type::bytea, cw.blob] order by cw.idx)
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
@@ -133,6 +133,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
jsonplus_serde = JsonPlusSerializer()
supports_pipeline: bool
def _load_checkpoint(
self,
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.2"
version = "2.0.3"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -24,6 +24,8 @@ from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import (
ERROR,
INTERRUPT,
RESUME,
SCHEDULED,
ChannelProtocol,
SendProtocol,
@@ -37,12 +39,13 @@ PendingWrite = Tuple[str, str, Any]
class CheckpointMetadata(TypedDict, total=False):
"""Metadata associated with a checkpoint."""
source: Literal["input", "loop", "update"]
source: Literal["input", "loop", "update", "fork"]
"""The source of the checkpoint.
- "input": The checkpoint was created from an input to invoke/stream/batch.
- "loop": The checkpoint was created from inside the pregel loop.
- "update": The checkpoint was created from a manual state update.
- "fork": The checkpoint was created as a copy of another checkpoint.
"""
step: int
"""The step number of the checkpoint.
@@ -449,4 +452,4 @@ Special writes (e.g. errors) map to negative indices, to avoid those writes from
conflicting with regular writes.
Each Checkpointer implementation should use this mapping in put_writes.
"""
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2}
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
@@ -1,10 +1,14 @@
import asyncio
import logging
import os
import pickle
import random
import shutil
from collections import defaultdict
from contextlib import AbstractAsyncContextManager, AbstractContextManager
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
from functools import partial
from types import TracebackType
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
from langchain_core.runnables import RunnableConfig
@@ -20,6 +24,8 @@ from langgraph.checkpoint.base import (
)
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
logger = logging.getLogger(__name__)
class MemorySaver(
BaseCheckpointSaver[str], AbstractContextManager, AbstractAsyncContextManager
@@ -68,13 +74,18 @@ class MemorySaver(
self,
*,
serde: Optional[SerializerProtocol] = None,
factory: Type[defaultdict] = defaultdict,
) -> None:
super().__init__(serde=serde)
self.storage = defaultdict(lambda: defaultdict(dict))
self.writes = defaultdict(dict)
self.storage = factory(lambda: defaultdict(dict))
self.writes = factory(dict)
self.stack = ExitStack()
if factory is not defaultdict:
self.stack.enter_context(self.storage) # type: ignore[arg-type]
self.stack.enter_context(self.writes) # type: ignore[arg-type]
def __enter__(self) -> "MemorySaver":
return self
return self.stack.__enter__()
def __exit__(
self,
@@ -82,10 +93,10 @@ class MemorySaver(
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
return
return self.stack.__exit__(exc_type, exc_value, traceback)
async def __aenter__(self) -> "MemorySaver":
return self
return self.stack.__enter__()
async def __aexit__(
self,
@@ -93,7 +104,7 @@ class MemorySaver(
__exc_value: Optional[BaseException],
__traceback: Optional[TracebackType],
) -> Optional[bool]:
return
return self.stack.__exit__(__exc_type, __exc_value, __traceback)
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the in-memory storage.
@@ -364,8 +375,12 @@ class MemorySaver(
checkpoint_ns = config["configurable"]["checkpoint_ns"]
checkpoint_id = config["configurable"]["checkpoint_id"]
outer_key = (thread_id, checkpoint_ns, checkpoint_id)
outer_writes_ = self.writes.get(outer_key)
for idx, (c, v) in enumerate(writes):
inner_key = (task_id, WRITES_IDX_MAP.get(c, idx))
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))
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
@@ -474,3 +489,76 @@ class MemorySaver(
next_v = current_v + 1
next_h = random.random()
return f"{next_v:032}.{next_h:016}"
class PersistentDict(defaultdict):
"""Persistent dictionary with an API compatible with shelve and anydbm.
The dict is kept in memory, so the dictionary operations run as fast as
a regular dictionary.
Write to disk is delayed until close or sync (similar to gdbm's fast mode).
Input file format is automatically discovered.
Output file format is selectable between pickle, json, and csv.
All three serialization formats are backed by fast C implementations.
Adapted from https://code.activestate.com/recipes/576642-persistent-dict-with-multiple-standard-file-format/
"""
def __init__(self, *args: Any, filename: str, **kwds: Any) -> None:
self.flag = "c" # r=readonly, c=create, or n=new
self.mode = None # None or an octal triple like 0644
self.format = "pickle" # 'csv', 'json', or 'pickle'
self.filename = filename
super().__init__(*args, **kwds)
def sync(self) -> None:
"Write dict to disk"
if self.flag == "r":
return
tempname = self.filename + ".tmp"
fileobj = open(tempname, "wb" if self.format == "pickle" else "w")
try:
self.dump(fileobj)
except Exception:
os.remove(tempname)
raise
finally:
fileobj.close()
shutil.move(tempname, self.filename) # atomic commit
if self.mode is not None:
os.chmod(self.filename, self.mode)
def close(self) -> None:
self.sync()
self.clear()
def __enter__(self) -> "PersistentDict":
return self
def __exit__(self, *exc_info: Any) -> None:
self.close()
def dump(self, fileobj: Any) -> None:
if self.format == "pickle":
pickle.dump(dict(self), fileobj, 2)
else:
raise NotImplementedError("Unknown format: " + repr(self.format))
def load(self) -> None:
# try formats from most restrictive to least restrictive
if self.flag == "n":
return
with open(self.filename, "rb" if self.format == "pickle" else "r") as fileobj:
for loader in (pickle.load,):
fileobj.seek(0)
try:
return self.update(loader(fileobj))
except EOFError:
return
except Exception:
logging.error(f"Failed to load file: {fileobj.name}")
raise
raise ValueError("File not in a supported f ormat")
@@ -11,6 +11,8 @@ from typing_extensions import Self
ERROR = "__error__"
SCHEDULED = "__scheduled__"
INTERRUPT = "__interrupt__"
RESUME = "__resume__"
TASKS = "__pregel_tasks"
Value = TypeVar("Value", covariant=True)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.2"
version = "2.0.5"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+7 -3
View File
@@ -314,7 +314,7 @@ async def test_cannot_put_empty_namespace() -> None:
assert store.get(("langgraph", "foo"), "bar") is None
class MockAsyncBatchedStore(AsyncBatchedBaseStore):
def __init__(self):
def __init__(self) -> None:
super().__init__()
self._store = InMemoryStore()
@@ -340,13 +340,17 @@ async def test_cannot_put_empty_namespace() -> None:
await async_store.aput(("langgraph", "foo"), "bar", doc)
await async_store.aput(("foo", "langgraph", "foo"), "bar", doc)
assert (await async_store.aget(("foo", "langgraph", "foo"), "bar")).value == doc
val = await async_store.aget(("foo", "langgraph", "foo"), "bar")
assert val is not None
assert val.value == doc
assert (await async_store.asearch(("foo", "langgraph", "foo")))[0].value == doc
await async_store.adelete(("foo", "langgraph", "foo"), "bar")
assert (await async_store.aget(("foo", "langgraph", "foo"), "bar")) is None
await async_store.abatch([PutOp(("valid", "namespace"), "key", doc)])
assert (await async_store.aget(("valid", "namespace"), "key")).value == doc
val = await async_store.aget(("valid", "namespace"), "key")
assert val is not None
assert val.value == doc
assert (await async_store.asearch(("valid", "namespace")))[0].value == doc
await async_store.adelete(("valid", "namespace"), "key")
assert (await async_store.aget(("valid", "namespace"), "key")) is None
+4 -2
View File
@@ -1,11 +1,13 @@
.PHONY: test lint format
.PHONY: test lint format test-integration
######################
# TESTING AND COVERAGE
######################
test:
poetry run pytest tests
poetry run pytest tests/unit_tests
test-integration:
poetry run pytest tests/integration_tests
######################
# LINTING AND FORMATTING
+155 -8
View File
@@ -6,6 +6,7 @@ from typing import Callable, Optional, Sequence
import click
import click.exceptions
from click import secho
import langgraph_cli.config
import langgraph_cli.docker
@@ -15,6 +16,8 @@ from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
from langgraph_cli.docker import DockerCapabilities
from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.progress import Progress
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
from langgraph_cli.version import __version__
OPT_DOCKER_COMPOSE = click.option(
"--docker-compose",
@@ -148,6 +151,7 @@ OPT_POSTGRES_URI = click.option(
@click.group()
@click.version_option(version=__version__, prog_name="LangGraph CLI")
def cli():
pass
@@ -167,9 +171,7 @@ def cli():
is_flag=True,
help="Wait for services to start before returning. Implies --detach",
)
@cli.command(
help="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."
)
@cli.command(help="🚀 Launch LangGraph API server.")
@log_command
def up(
config: pathlib.Path,
@@ -334,7 +336,7 @@ def _build(
)
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
@cli.command(
help="Build langgraph API server docker image",
help="📦 Build LangGraph API server Docker image.",
context_settings=dict(
ignore_unknown_options=True,
),
@@ -357,14 +359,86 @@ def build(
)
def _get_docker_ignore_content() -> str:
"""Return the content of a .dockerignore file.
This file is used to exclude files and directories from the Docker build context.
It may be overly broad, but it's better to be safe than sorry.
The main goal is to exclude .env files by default.
"""
return """\
# Ignore node_modules and other dependency directories
node_modules
bower_components
vendor
# Ignore logs and temporary files
*.log
*.tmp
*.swp
# Ignore .env files and other environment files
.env
.env.*
*.local
# Ignore git-related files
.git
.gitignore
# Ignore Docker-related files and configs
.dockerignore
docker-compose.yml
# Ignore build and cache directories
dist
build
.cache
__pycache__
# Ignore IDE and editor configurations
.vscode
.idea
*.sublime-project
*.sublime-workspace
.DS_Store # macOS-specific
# Ignore test and coverage files
coverage
*.coverage
*.test.js
*.spec.js
tests
"""
@OPT_CONFIG
@click.argument("save_path", type=click.Path(resolve_path=True))
@cli.command(help="Generate a Dockerfile for langgraph API server")
@cli.command(
help="🐳 Generate a Dockerfile for the LangGraph API server, with Docker Compose options."
)
@click.option(
# Add a flag for adding a docker-compose.yml file as part of the output
"--add-docker-compose",
help=(
"Add additional files for running the LangGraph API server with "
"docker-compose. These files include a docker-compose.yml, .env file, "
"and a .dockerignore file."
),
is_flag=True,
)
@log_command
def dockerfile(save_path: pathlib.Path, config: pathlib.Path):
with open(config) as f:
def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -> None:
save_path = pathlib.Path(save_path).absolute()
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
with open(config, encoding="utf-8") as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
with open(save_path, "w") as f:
secho("✅ Configuration validated!", fg="green")
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
with open(str(save_path), "w", encoding="utf-8") as f:
f.write(
langgraph_cli.config.config_to_docker(
config,
@@ -374,6 +448,79 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path):
else "langchain/langgraph-api",
)
)
secho("✅ Created: Dockerfile", fg="green")
if add_docker_compose:
# Add docker compose and related files
# Add .dockerignore file in the same directory as the Dockerfile
with open(str(save_path.parent / ".dockerignore"), "w", encoding="utf-8") as f:
f.write(_get_docker_ignore_content())
secho("✅ Created: .dockerignore", fg="green")
# Generate a docker-compose.yml file
path = str(save_path.parent / "docker-compose.yml")
with open(path, "w", encoding="utf-8") as f:
with Runner() as runner:
capabilities = langgraph_cli.docker.check_capabilities(runner)
compose_dict = langgraph_cli.docker.compose_as_dict(
capabilities,
port=8123,
)
# Add .env file to the docker-compose.yml for the langgraph-api service
compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
# Add the Dockerfile to the build context
compose_dict["services"]["langgraph-api"]["build"] = {
"context": ".",
"dockerfile": save_path.name,
}
f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
secho("✅ Created: docker-compose.yml", fg="green")
# Check if the .env file exists in the same directory as the Dockerfile
if not (save_path.parent / ".env").exists():
# Also add an empty .env file
with open(str(save_path.parent / ".env"), "w", encoding="utf-8") as f:
f.writelines(
[
"# Uncomment the following line to add your LangSmith API key",
"\n",
"# LANGSMITH_API_KEY=your-api-key",
"\n",
"# Or if you have a LangGraph Cloud license key, "
"then uncomment the following line: ",
"\n",
"# LANGGRAPH_CLOUD_LICENSE_KEY=your-license-key",
"\n",
"# Add any other environment variables go below...",
]
)
secho("✅ Created: .env", fg="green")
else:
# Do nothing since the .env file already exists. Not a great
# idea to overwrite in case the user has added custom env vars set
# in the .env file already.
secho(" Skipped: .env. It already exists!", fg="yellow")
secho(
f"🎉 Files generated successfully at path {save_path.parent}!",
fg="cyan",
bold=True,
)
@click.argument("path", required=False)
@click.option(
"--template",
type=str,
help=TEMPLATE_HELP_STRING,
)
@cli.command("new", help="🌱 Create a new LangGraph project from a template.")
@log_command
def new(path: Optional[str], template: Optional[str]) -> None:
"""Create a new LangGraph project from a template."""
return create_new(path, template)
def prepare_args_and_stdin(
+26 -2
View File
@@ -289,17 +289,41 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
faux_path = f"/deps/{config_path.parent.name}"
def test_file(file_name):
full_path = config_path.parent / file_name
try:
return full_path.is_file()
except OSError:
return False
npm, yarn, pnpm = [
test_file("package-lock.json"),
test_file("yarn.lock"),
test_file("pnpm-lock.yaml"),
]
if yarn:
install_cmd = "yarn install --frozen-lockfile"
elif pnpm:
install_cmd = "pnpm i --frozen-lockfile"
elif npm:
install_cmd = "npm ci"
else:
install_cmd = "npm i"
return f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
ADD . {faux_path}
RUN cd {faux_path} && yarn install --frozen-lockfile
RUN cd {faux_path} && {install_cmd}
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
WORKDIR {faux_path}"""
WORKDIR {faux_path}
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
+145 -93
View File
@@ -12,34 +12,6 @@ DEFAULT_POSTGRES_URI = (
"postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable"
)
REDIS = """
langgraph-redis:
image: redis:6
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
"""
DB = """
langgraph-postgres:
image: postgres:16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
test: pg_isready -U postgres
start_period: 10s
timeout: 1s
retries: 5
"""
class Version(NamedTuple):
major: int
@@ -116,28 +88,149 @@ def check_capabilities(runner) -> DockerCapabilities:
def debugger_compose(
*, port: Optional[int] = None, base_url: Optional[str] = None
) -> str:
) -> dict:
if port is None:
return ""
compose_str = """
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{port}:3968"
"""
config = {
"langgraph-debugger": {
"image": "langchain/langgraph-debugger",
"restart": "on-failure",
"depends_on": {
"langgraph-postgres": {"condition": "service_healthy"},
},
"ports": [f'"{port}:3968"'],
}
}
if base_url:
compose_str += """
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {base_url}
"""
config["langgraph-debugger"]["environment"] = {
"VITE_STUDIO_LOCAL_GRAPH_URL": base_url
}
return compose_str.format(port=port, base_url=base_url)
return config
# Function to convert dictionary to YAML
def dict_to_yaml(d: dict, *, indent: int = 0) -> str:
"""Convert a dictionary to a YAML string."""
yaml_str = ""
for idx, (key, value) in enumerate(d.items()):
# Format things in a visually appealing way
# Use an extra newline for top-level keys only
if idx >= 1 and indent < 2:
yaml_str += "\n"
space = " " * indent
if isinstance(value, dict):
yaml_str += f"{space}{key}:\n" + dict_to_yaml(value, indent=indent + 1)
elif isinstance(value, list):
yaml_str += f"{space}{key}:\n"
for item in value:
yaml_str += f"{space} - {item}\n"
else:
yaml_str += f"{space}{key}: {value}\n"
return yaml_str
def compose_as_dict(
capabilities: DockerCapabilities,
*,
port: int,
debugger_port: Optional[int] = None,
debugger_base_url: Optional[str] = None,
# postgres://user:password@host:port/database?option=value
postgres_uri: Optional[str] = None,
) -> dict:
"""Create a docker compose file as a dictionary in YML style."""
if postgres_uri is None:
include_db = True
postgres_uri = DEFAULT_POSTGRES_URI
else:
include_db = False
# The services below are defined in a non-intuitive order to match
# the existing unit tests for this function.
# It's fine to re-order just requires updating the unit tests, so it should
# be done with caution.
# Define the Redis service first as per the test order
services = {
"langgraph-redis": {
"image": "redis:6",
"healthcheck": {
"test": "redis-cli ping",
"interval": "5s",
"timeout": "1s",
"retries": 5,
},
}
}
# Add Postgres service before langgraph-api if it is needed
if include_db:
services["langgraph-postgres"] = {
"image": "postgres:16",
"ports": ['"5433:5432"'],
"environment": {
"POSTGRES_DB": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "postgres",
},
"volumes": ["langgraph-data:/var/lib/postgresql/data"],
"healthcheck": {
"test": "pg_isready -U postgres",
"start_period": "10s",
"timeout": "1s",
"retries": 5,
},
}
if capabilities.healthcheck_start_interval:
services["langgraph-postgres"]["healthcheck"]["interval"] = "60s"
services["langgraph-postgres"]["healthcheck"]["start_interval"] = "1s"
else:
services["langgraph-postgres"]["healthcheck"]["interval"] = "5s"
# Add optional debugger service if debugger_port is specified
if debugger_port:
services["langgraph-debugger"] = debugger_compose(
port=debugger_port, base_url=debugger_base_url
)["langgraph-debugger"]
# Add langgraph-api service
services["langgraph-api"] = {
"ports": [f'"{port}:8000"'],
"depends_on": {
"langgraph-redis": {"condition": "service_healthy"},
},
"environment": {
"REDIS_URI": "redis://langgraph-redis:6379",
"POSTGRES_URI": postgres_uri,
},
}
# If Postgres is included, add it to the dependencies of langgraph-api
if include_db:
services["langgraph-api"]["depends_on"]["langgraph-postgres"] = {
"condition": "service_healthy"
}
# Additional healthcheck for langgraph-api if required
if capabilities.healthcheck_start_interval:
services["langgraph-api"]["healthcheck"] = {
"test": "python /api/healthcheck.py",
"interval": "60s",
"start_interval": "1s",
"start_period": "10s",
}
# Final compose dictionary with volumes included if needed
compose_dict = {}
if include_db:
compose_dict["volumes"] = {"langgraph-data": {"driver": "local"}}
compose_dict["services"] = services
return compose_dict
def compose(
@@ -149,54 +242,13 @@ def compose(
# postgres://user:password@host:port/database?option=value
postgres_uri: Optional[str] = None,
) -> str:
if postgres_uri is None:
include_db = True
postgres_uri = DEFAULT_POSTGRES_URI
else:
include_db = False
db = DB.format() if include_db else ""
volumes = (
"""volumes:
langgraph-data:
driver: local
"""
if include_db
else ""
"""Create a docker compose file as a string."""
compose_content = compose_as_dict(
capabilities,
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_base_url,
postgres_uri=postgres_uri,
)
if db:
if capabilities.healthcheck_start_interval:
db += """
interval: 60s
start_interval: 1s"""
else:
db += """
interval: 5s"""
compose_str = f"""{volumes}services:
{REDIS}
{db}
{debugger_compose(port=debugger_port, base_url=debugger_base_url)}
langgraph-api:
ports:
- "{port}:8000\"
depends_on:
langgraph-redis:
condition: service_healthy"""
if include_db:
compose_str += """
langgraph-postgres:
condition: service_healthy"""
compose_str += f"""
environment:
REDIS_URI: redis://langgraph-redis:6379
POSTGRES_URI: {postgres_uri}
"""
if capabilities.healthcheck_start_interval:
compose_str += """ healthcheck:
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s"""
compose_str = dict_to_yaml(compose_content)
return compose_str
+223
View File
@@ -0,0 +1,223 @@
import os
import shutil
import sys
from io import BytesIO
from typing import Dict, Optional
from urllib import error, request
from zipfile import ZipFile
import click
TEMPLATES: Dict[str, Dict[str, str]] = {
"New LangGraph Project": {
"description": "A simple, minimal chatbot with memory.",
"python": "https://github.com/langchain-ai/new-langgraph-project/archive/refs/heads/main.zip",
"js": "https://github.com/langchain-ai/new-langgraphjs-project/archive/refs/heads/main.zip",
},
"ReAct Agent": {
"description": "A simple agent that can be flexibly extended to many tools.",
"python": "https://github.com/langchain-ai/react-agent/archive/refs/heads/main.zip",
"js": "https://github.com/langchain-ai/react-agent-js/archive/refs/heads/main.zip",
},
"Memory Agent": {
"description": "A ReAct-style agent with an additional tool to store memories for use across conversational threads.",
"python": "https://github.com/langchain-ai/memory-agent/archive/refs/heads/main.zip",
"js": "https://github.com/langchain-ai/memory-agent-js/archive/refs/heads/main.zip",
},
"Retrieval Agent": {
"description": "An agent that includes a retrieval-based question-answering system.",
"python": "https://github.com/langchain-ai/retrieval-agent-template/archive/refs/heads/main.zip",
"js": "https://github.com/langchain-ai/retrieval-agent-template-js/archive/refs/heads/main.zip",
},
"Data-enrichment Agent": {
"description": "An agent that performs web searches and organizes its findings into a structured format.",
"python": "https://github.com/langchain-ai/data-enrichment/archive/refs/heads/main.zip",
"js": "https://github.com/langchain-ai/data-enrichment-js/archive/refs/heads/main.zip",
},
}
# Generate TEMPLATE_IDS programmatically
TEMPLATE_ID_TO_CONFIG = {
f"{name.lower().replace(' ', '-')}-{lang}": (name, lang, url)
for name, versions in TEMPLATES.items()
for lang, url in versions.items()
if lang in {"python", "js"}
}
TEMPLATE_IDS = list(TEMPLATE_ID_TO_CONFIG.keys())
TEMPLATE_HELP_STRING = (
"The name of the template to use. Available options:\n"
+ "\n".join(f"{id_}" for id_ in TEMPLATE_ID_TO_CONFIG)
)
def _choose_template() -> str:
"""Presents a list of templates to the user and prompts them to select one.
Returns:
str: The URL of the selected template.
"""
click.secho("🌟 Please select a template:", bold=True, fg="yellow")
for idx, (template_name, template_info) in enumerate(TEMPLATES.items(), 1):
click.secho(f"{idx}. ", nl=False, fg="cyan")
click.secho(template_name, fg="cyan", nl=False)
click.secho(f" - {template_info['description']}", fg="white")
# Get the template choice from the user, defaulting to the first template if blank
template_choice: Optional[int] = click.prompt(
"Enter the number of your template choice (default is 1)",
type=int,
default=1,
show_default=False,
)
template_keys = list(TEMPLATES.keys())
if 1 <= template_choice <= len(template_keys):
selected_template: str = template_keys[template_choice - 1]
else:
click.secho("❌ Invalid choice. Please try again.", fg="red")
return _choose_template()
# Prompt the user to choose between Python or JS/TS version
click.secho(
f"\nYou selected: {selected_template} - {TEMPLATES[selected_template]['description']}",
fg="green",
)
version_choice: int = click.prompt(
"Choose language (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
)
if version_choice == 1:
return TEMPLATES[selected_template]["python"]
elif version_choice == 2:
return TEMPLATES[selected_template]["js"]
else:
click.secho("❌ Invalid choice. Please try again.", fg="red")
return _choose_template()
def _download_repo_with_requests(repo_url: str, path: str) -> None:
"""Download a ZIP archive from the given URL and extracts it to the specified path.
Args:
repo_url (str): The URL of the repository to download.
path (str): The path where the repository should be extracted.
"""
click.secho("📥 Attempting to download repository as a ZIP archive...", fg="yellow")
click.secho(f"URL: {repo_url}", fg="yellow")
try:
with request.urlopen(repo_url) as response:
if response.status == 200:
with ZipFile(BytesIO(response.read())) as zip_file:
zip_file.extractall(path)
# Move extracted contents to path
for item in os.listdir(path):
if item.endswith("-main"):
extracted_dir = os.path.join(path, item)
for filename in os.listdir(extracted_dir):
shutil.move(os.path.join(extracted_dir, filename), path)
shutil.rmtree(extracted_dir)
click.secho(
f"✅ Downloaded and extracted repository to {path}", fg="green"
)
except error.HTTPError as e:
click.secho(
f"❌ Error: Failed to download repository.\n" f"Details: {e}\n",
fg="red",
bold=True,
err=True,
)
sys.exit(1)
def _get_template_url(template_name: str) -> Optional[str]:
"""
Retrieves the template URL based on the provided template name.
Args:
template_name (str): The name of the template.
Returns:
Optional[str]: The URL of the template if found, else None.
"""
if template_name in TEMPLATES:
click.secho(f"Template selected: {template_name}", fg="green")
version_choice: int = click.prompt(
"Choose version (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
)
if version_choice == 1:
return TEMPLATES[template_name]["python"]
elif version_choice == 2:
return TEMPLATES[template_name]["js"]
else:
click.secho("❌ Invalid choice. Please try again.", fg="red")
return None
else:
click.secho(
f"Template '{template_name}' not found. Please select from the available options.",
fg="red",
)
return None
def create_new(path: Optional[str], template: Optional[str]) -> None:
"""Create a new LangGraph project at the specified PATH using the chosen TEMPLATE.
Args:
path (Optional[str]): The path where the new project will be created.
template (Optional[str]): The name of the template to use.
"""
# Prompt for path if not provided
if not path:
path = click.prompt(
"📂 Please specify the path to create the application", default="."
)
path = os.path.abspath(path) # Ensure path is absolute
# Check if path exists and is not empty
if os.path.exists(path) and os.listdir(path):
click.secho(
"❌ The specified directory already exists and is not empty. "
"Aborting to prevent overwriting files.",
fg="red",
bold=True,
)
sys.exit(1)
# Get template URL either from command-line argument or
# through interactive selection
if template:
if template not in TEMPLATE_ID_TO_CONFIG:
# Format available options in a readable way with descriptions
template_options = ""
for id_ in TEMPLATE_IDS:
name, lang, _ = TEMPLATE_ID_TO_CONFIG[id_]
description = TEMPLATES[name]["description"]
# Add each template option with color formatting
template_options += (
click.style("- ", fg="yellow", bold=True)
+ click.style(f"{id_}", fg="cyan")
+ click.style(f": {description}", fg="white")
+ "\n"
)
# Display error message with colors and formatting
click.secho("❌ Error:", fg="red", bold=True, nl=False)
click.secho(f" Template '{template}' not found.", fg="red")
click.secho(
"Please select from the available options:\n", fg="yellow", bold=True
)
click.secho(template_options, fg="cyan")
sys.exit(1)
_, _, template_url = TEMPLATE_ID_TO_CONFIG[template]
else:
template_url = _choose_template()
# Download and extract the template
_download_repo_with_requests(template_url, path)
click.secho(f"🎉 New project created at {path}", fg="green", bold=True)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.53"
version = "0.1.54"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -0,0 +1,13 @@
import pytest
import requests
from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
@pytest.mark.parametrize("template_key", TEMPLATE_ID_TO_CONFIG.keys())
def test_template_urls_work(template_key: str) -> None:
"""Integration test to verify that all template URLs are reachable."""
_, _, template_url = TEMPLATE_ID_TO_CONFIG[template_key]
response = requests.head(template_url)
# Returns 302 on a successful HEAD request
assert response.status_code == 302, f"URL {template_url} is not reachable."
+244
View File
@@ -0,0 +1,244 @@
import json
import pathlib
import shutil
import tempfile
from contextlib import contextmanager
from pathlib import Path
from click.testing import CliRunner
from langgraph_cli.cli import cli, prepare_args_and_stdin
from langgraph_cli.config import Config, validate_config
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
from langgraph_cli.util import clean_empty_lines
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
version_docker=Version(26, 1, 1),
version_compose=Version(2, 27, 0),
healthcheck_start_interval=True,
)
@contextmanager
def temporary_config_folder(config_content: dict):
# Create a temporary directory
temp_dir = tempfile.mkdtemp()
try:
# Define the path for the config.json file
config_path = Path(temp_dir) / "config.json"
# Write the provided dictionary content to config.json
with open(config_path, "w", encoding="utf-8") as config_file:
json.dump(config_content, config_file)
# Yield the temporary directory path for use within the context
yield config_path.parent
finally:
# Cleanup the temporary directory and its contents
shutil.rmtree(temp_dir)
def test_prepare_args_and_stdin() -> None:
# this basically serves as an end-to-end test for using config and docker helpers
config_path = pathlib.Path("./langgraph.json")
config = validate_config(
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
)
port = 8000
debugger_port = 8001
debugger_graph_url = f"http://127.0.0.1:{port}"
actual_args, actual_stdin = prepare_args_and_stdin(
capabilities=DEFAULT_DOCKER_CAPABILITIES,
config_path=config_path,
config=config,
docker_compose=pathlib.Path("custom-docker-compose.yml"),
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_graph_url,
watch=True,
)
expected_args = [
"--project-directory",
".",
"-f",
"custom-docker-compose.yml",
"-f",
"-",
]
expected_stdin = f"""volumes:
langgraph-data:
driver: local
services:
langgraph-redis:
image: redis:6
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
langgraph-postgres:
image: postgres:16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
test: pg_isready -U postgres
start_period: 10s
timeout: 1s
retries: 5
interval: 60s
start_interval: 1s
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
langgraph-api:
ports:
- "8000:8000"
depends_on:
langgraph-redis:
condition: service_healthy
langgraph-postgres:
condition: service_healthy
environment:
REDIS_URI: redis://langgraph-redis:6379
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
healthcheck:
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s
pull_policy: build
build:
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
WORKDIR /deps/
develop:
watch:
- path: langgraph.json
action: rebuild
- path: .
action: rebuild\
"""
assert actual_args == expected_args
assert clean_empty_lines(actual_stdin) == expected_stdin
def test_version_option() -> None:
"""Test the --version option of the CLI."""
runner = CliRunner()
result = runner.invoke(cli, ["--version"])
# Verify that the command executed successfully
assert result.exit_code == 0, "Expected exit code 0 for --version option"
# Check that the output contains the correct version information
assert (
"LangGraph CLI, version" in result.output
), "Expected version information in output"
def test_dockerfile_command_basic() -> None:
"""Test the 'dockerfile' command with basic configuration."""
runner = CliRunner()
config_content = {
"node_version": "20", # Add any other necessary configuration fields
"graphs": {"agent": "agent.py:graph"},
}
with temporary_config_folder(config_content) as temp_dir:
save_path = temp_dir / "Dockerfile"
result = runner.invoke(
cli,
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
)
# Assert command was successful
assert result.exit_code == 0, result.output
assert "✅ Created: Dockerfile" in result.output
# Check if Dockerfile was created
assert save_path.exists()
def test_dockerfile_command_with_docker_compose() -> None:
"""Test the 'dockerfile' command with Docker Compose configuration."""
runner = CliRunner()
config_content = {
"dependencies": ["./my_agent"],
"graphs": {"agent": "./my_agent/agent.py:graph"},
"env": ".env",
}
with temporary_config_folder(config_content) as temp_dir:
save_path = temp_dir / "Dockerfile"
# Add agent.py file
agent_path = temp_dir / "my_agent" / "agent.py"
agent_path.parent.mkdir(parents=True, exist_ok=True)
agent_path.touch()
result = runner.invoke(
cli,
[
"dockerfile",
str(save_path),
"--config",
str(temp_dir / "config.json"),
"--add-docker-compose",
],
)
# Assert command was successful
assert result.exit_code == 0
assert "✅ Created: Dockerfile" in result.output
assert "✅ Created: .dockerignore" in result.output
assert "✅ Created: docker-compose.yml" in result.output
assert (
"✅ Created: .env" in result.output or " Skipped: .env" in result.output
)
assert "🎉 Files generated successfully" in result.output
# Check if Dockerfile, .dockerignore, docker-compose.yml, and .env were created
assert save_path.exists()
assert (temp_dir / ".dockerignore").exists()
assert (temp_dir / "docker-compose.yml").exists()
assert (temp_dir / ".env").exists() or " Skipped: .env" in result.output
def test_dockerfile_command_with_bad_config() -> None:
"""Test the 'dockerfile' command with basic configuration."""
runner = CliRunner()
config_content = {
"node_version": "20" # Add any other necessary configuration fields
}
with temporary_config_folder(config_content) as temp_dir:
save_path = temp_dir / "Dockerfile"
result = runner.invoke(
cli,
["dockerfile", str(save_path), "--config", str(temp_dir / "conf.json")],
)
# Assert command was successful
assert result.exit_code == 2
assert "conf.json' does not exist" in result.output
@@ -0,0 +1,70 @@
"""Unit tests for the 'new' CLI command.
This command creates a new LangGraph project using a specified template.
"""
import os
from io import BytesIO
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch
from urllib import request
from zipfile import ZipFile
from click.testing import CliRunner
from langgraph_cli.cli import cli
from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
@patch.object(request, "urlopen")
def test_create_new_with_mocked_download(mock_urlopen: MagicMock) -> None:
"""Test the 'new' CLI command with a mocked download response using urllib."""
# Mock the response content to simulate a ZIP file
mock_zip_content = BytesIO()
with ZipFile(mock_zip_content, "w") as mock_zip:
mock_zip.writestr("test-file.txt", "Test content.")
# Create a mock response that behaves like a context manager
mock_response = MagicMock()
mock_response.read.return_value = mock_zip_content.getvalue()
mock_response.__enter__.return_value = mock_response # Setup enter context
mock_response.status = 200
mock_urlopen.return_value = mock_response
with TemporaryDirectory() as temp_dir:
runner = CliRunner()
template = next(
iter(TEMPLATE_ID_TO_CONFIG)
) # Select the first template for the test
result = runner.invoke(cli, ["new", temp_dir, "--template", template])
# Verify CLI command execution and success
assert result.exit_code == 0, result.output
assert (
"New project created" in result.output
), "Expected success message in output."
# Verify that the directory is not empty
assert os.listdir(temp_dir), "Expected files to be created in temp directory."
# Check for a known file in the extracted content
extracted_files = [f.name for f in Path(temp_dir).glob("*")]
assert (
"test-file.txt" in extracted_files
), "Expected 'test-file.txt' in the extracted content."
def test_invalid_template_id() -> None:
"""Test that an invalid template ID passed via CLI results in a graceful error."""
runner = CliRunner()
result = runner.invoke(
cli, ["new", "dummy_path", "--template", "invalid-template-id"]
)
# Verify the command failed and proper message is displayed
assert result.exit_code != 0, "Expected non-zero exit code for invalid template."
assert (
"Template 'invalid-template-id' not found" in result.output
), "Expected error message in output."
+16
View File
@@ -0,0 +1,16 @@
import os
from unittest.mock import patch
import pytest
@pytest.fixture(autouse=True)
def disable_analytics_env() -> None:
"""Disable analytics for unit tests LANGGRAPH_CLI_NO_ANALYTICS."""
# First check if the environment variable is already set, if so, log a warning prior
# to overriding it.
if "LANGGRAPH_CLI_NO_ANALYTICS" in os.environ:
print("⚠️ LANGGRAPH_CLI_NO_ANALYTICS is set. Overriding it for the test.")
with patch.dict(os.environ, {"LANGGRAPH_CLI_NO_ANALYTICS": "0"}):
yield
-117
View File
@@ -1,117 +0,0 @@
import pathlib
from langgraph_cli.cli import prepare_args_and_stdin
from langgraph_cli.config import Config, validate_config
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
from langgraph_cli.util import clean_empty_lines
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
version_docker=Version(26, 1, 1),
version_compose=Version(2, 27, 0),
healthcheck_start_interval=True,
)
def test_prepare_args_and_stdin():
# this basically serves as an end-to-end test for using config and docker helpers
config_path = pathlib.Path("./langgraph.json")
config = validate_config(
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
)
port = 8000
debugger_port = 8001
debugger_graph_url = f"http://127.0.0.1:{port}"
actual_args, actual_stdin = prepare_args_and_stdin(
capabilities=DEFAULT_DOCKER_CAPABILITIES,
config_path=config_path,
config=config,
docker_compose="custom-docker-compose.yml",
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_graph_url,
watch=True,
)
expected_args = [
"--project-directory",
".",
"-f",
"custom-docker-compose.yml",
"-f",
"-",
]
expected_stdin = f"""volumes:
langgraph-data:
driver: local
services:
langgraph-redis:
image: redis:6
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
langgraph-postgres:
image: postgres:16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
test: pg_isready -U postgres
start_period: 10s
timeout: 1s
retries: 5
interval: 60s
start_interval: 1s
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
langgraph-api:
ports:
- "8000:8000"
depends_on:
langgraph-redis:
condition: service_healthy
langgraph-postgres:
condition: service_healthy
environment:
REDIS_URI: redis://langgraph-redis:6379
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
healthcheck:
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s
pull_policy: build
build:
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
WORKDIR /deps/
develop:
watch:
- path: langgraph.json
action: rebuild
- path: .
action: rebuild\
"""
assert actual_args == expected_args
assert clean_empty_lines(actual_stdin) == expected_stdin
+3 -2
View File
@@ -250,9 +250,10 @@ def test_config_to_docker_nodejs():
ARG meow
ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && yarn install --frozen-lockfile
RUN cd /deps/unit_tests && npm i
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
WORKDIR /deps/unit_tests"""
WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+1 -1
View File
@@ -49,7 +49,7 @@ test:
exit $$EXIT_CODE
test_watch:
make start-postgres && poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
make start-postgres && poetry run ptw . -- --ff -vv -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
EXIT_CODE=$$?; \
make stop-postgres; \
exit $$EXIT_CODE
+13
View File
@@ -1,4 +1,5 @@
import sys
from os import getenv
from types import MappingProxyType
from typing import Any, Literal, Mapping, cast
@@ -10,6 +11,7 @@ from langgraph.types import Interrupt, Send # noqa: F401
# --- Empty read-only containers ---
EMPTY_MAP: Mapping[str, Any] = MappingProxyType({})
EMPTY_SEQ: tuple[str, ...] = tuple()
MISSING = object()
# --- Public constants ---
TAG_NOSTREAM = sys.intern("langsmith:nostream")
@@ -20,12 +22,16 @@ START = sys.intern("__start__")
"""The first (maybe virtual) node in graph-style Pregel."""
END = sys.intern("__end__")
"""The last (maybe virtual) node in graph-style Pregel."""
SELF = sys.intern("__self__")
"""The implicit branch that handles each node's Control values."""
# --- Reserved write keys ---
INPUT = sys.intern("__input__")
# for values passed as input to the graph
INTERRUPT = sys.intern("__interrupt__")
# for dynamic interrupts raised by nodes
RESUME = sys.intern("__resume__")
# for values passed to resume a node after an interrupt
ERROR = sys.intern("__error__")
# for errors raised by nodes
NO_WRITES = sys.intern("__no_writes__")
@@ -67,6 +73,8 @@ CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns")
# holds the current checkpoint_ns, "" for root graph
CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
# callback to be called when a node is finished
CONFIG_KEY_RESUME_VALUE = sys.intern("__pregel_resume_value")
# holds the value that "answers" an interrupt() call
# --- Other constants ---
PUSH = sys.intern("__pregel_push")
@@ -79,12 +87,17 @@ NS_END = sys.intern(":")
# for checkpoint_ns, for each level, separates the namespace from the task_id
CONF = cast(Literal["configurable"], sys.intern("configurable"))
# key for the configurable dict in RunnableConfig
FF_SEND_V2 = getenv("LANGGRAPH_FF_SEND_V2", "false").lower() == "true"
# temporary flag to enable new Send semantics
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
# the task_id to use for writes that are not associated with a task
RESERVED = {
TAG_HIDDEN,
# reserved write keys
INPUT,
INTERRUPT,
RESUME,
ERROR,
NO_WRITES,
SCHEDULED,
+1 -1
View File
@@ -70,7 +70,7 @@ class NodeInterrupt(GraphInterrupt):
"""Raised by a node to interrupt execution."""
def __init__(self, value: Any) -> None:
super().__init__([Interrupt(value)])
super().__init__([Interrupt(value=value)])
class GraphDelegate(Exception):
+2 -1
View File
@@ -1,12 +1,13 @@
from langgraph.graph.graph import END, START, Graph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.graph.state import StateGraph
from langgraph.graph.state import GraphCommand, StateGraph
__all__ = [
"END",
"START",
"Graph",
"StateGraph",
"GraphCommand",
"MessageGraph",
"add_messages",
"MessagesState",
+19 -13
View File
@@ -27,6 +27,7 @@ from typing_extensions import Self
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.constants import (
EMPTY_SEQ,
END,
NS_END,
NS_SEP,
@@ -47,6 +48,7 @@ logger = logging.getLogger(__name__)
class NodeSpec(NamedTuple):
runnable: Runnable
metadata: Optional[dict[str, Any]] = None
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
class Branch(NamedTuple):
@@ -123,7 +125,7 @@ class Branch(NamedTuple):
result: Any,
config: RunnableConfig,
) -> Union[Runnable, Any]:
if not isinstance(result, list):
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations: Sequence[Union[Send, str]] = [
@@ -364,6 +366,9 @@ class Graph:
for node in self.nodes:
if node != start and node != branch.then:
all_sources.add(node)
for name, spec in self.nodes.items():
if spec.ends:
all_sources.add(name)
# validate sources
for source in all_sources:
if source not in self.nodes and source != START:
@@ -387,6 +392,9 @@ class Graph:
for node in self.nodes:
if node != start and node != branch.then:
all_targets.add(node)
for name, spec in self.nodes.items():
if spec.ends:
all_targets.update(spec.ends)
# validate targets
for node in self.nodes:
if node not in all_targets:
@@ -565,18 +573,12 @@ class CompiledGraph(Pregel):
metadata["__interrupt"] = "before"
elif key in self.interrupt_after_nodes:
metadata["__interrupt"] = "after"
if xray:
subgraph = (
subgraphs[key].get_graph(
config=config,
xray=xray - 1
if isinstance(xray, int)
and not isinstance(xray, bool)
and xray > 0
else xray,
)
if key in subgraphs
else node.get_graph(config=config)
if xray and key in subgraphs:
subgraph = subgraphs[key].get_graph(
config=config,
xray=xray - 1
if isinstance(xray, int) and not isinstance(xray, bool) and xray > 0
else xray,
)
subgraph.trim_first_node()
subgraph.trim_last_node()
@@ -620,5 +622,9 @@ class CompiledGraph(Pregel):
)
if branch.then is not None:
add_edge(end, branch.then)
for key, n in self.builder.nodes.items():
if n.ends:
for end in n.ends:
add_edge(key, end, conditional=True)
return graph
+173 -34
View File
@@ -1,12 +1,15 @@
import dataclasses
import inspect
import logging
import typing
import warnings
from functools import partial
from inspect import isclass, isfunction, ismethod, signature
from types import FunctionType
from typing import (
Any,
Callable,
Generic,
Literal,
NamedTuple,
Optional,
@@ -14,6 +17,7 @@ from typing import (
Type,
Union,
cast,
get_args,
get_origin,
get_type_hints,
overload,
@@ -32,7 +36,7 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.channels.named_barrier_value import NamedBarrierValue
from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
from langgraph.managed.base import (
@@ -46,10 +50,10 @@ from langgraph.managed.base import (
from langgraph.pregel.read import ChannelRead, PregelNode
from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry
from langgraph.store.base import BaseStore
from langgraph.types import All, Checkpointer, RetryPolicy
from langgraph.types import _DC_KWARGS, All, Checkpointer, Command, N, RetryPolicy
from langgraph.utils.fields import get_field_default
from langgraph.utils.pydantic import create_model
from langgraph.utils.runnable import coerce_to_runnable
from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable
logger = logging.getLogger(__name__)
@@ -66,11 +70,37 @@ def _warn_invalid_state_schema(schema: Union[Type[Any], Any]) -> None:
)
def _get_node_name(node: RunnableLike) -> str:
if isinstance(node, Runnable):
return node.get_name()
elif callable(node):
return getattr(node, "__name__", node.__class__.__name__)
else:
raise TypeError(f"Unsupported node type: {type(node)}")
@dataclasses.dataclass(**_DC_KWARGS)
class GraphCommand(Generic[N], Command[N]):
"""One or more commands to update a StateGraph's state and go to, or send messages to nodes."""
goto: Union[str, Sequence[str]] = ()
def __repr__(self) -> str:
# get all non-None values
contents = ", ".join(
f"{key}={value!r}"
for key, value in dataclasses.asdict(self).items()
if value
)
return f"Command({contents})"
class StateNodeSpec(NamedTuple):
runnable: Runnable
metadata: Optional[dict[str, Any]]
input: Type[Any]
retry_policy: Optional[RetryPolicy]
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
class StateGraph(Graph):
@@ -222,7 +252,7 @@ class StateGraph(Graph):
ValueError: If the key is already being used as a state key.
Returns:
None
StateGraph
"""
...
@@ -246,7 +276,7 @@ class StateGraph(Graph):
ValueError: If the key is already being used as a state key.
Returns:
None
StateGraph
"""
...
@@ -299,7 +329,7 @@ class StateGraph(Graph):
```
Returns:
None
StateGraph
"""
if not isinstance(node, str):
action = node
@@ -338,8 +368,33 @@ class StateGraph(Graph):
f"'{character}' is a reserved character and is not allowed in the node names."
)
if input is None:
input = _get_input_schema_from_type_hint(action)
ends = EMPTY_SEQ
try:
if (isfunction(action) or ismethod(getattr(action, "__call__", None))) and (
hints := get_type_hints(getattr(action, "__call__"))
or get_type_hints(action)
):
if input is None:
first_parameter_name = next(
iter(
inspect.signature(
cast(FunctionType, action)
).parameters.keys()
)
)
if input_hint := hints.get(first_parameter_name):
if isinstance(input_hint, type) and get_type_hints(input_hint):
input = input_hint
if (
(rtn := hints.get("return"))
and get_origin(rtn) in (Command, GraphCommand)
and (rargs := get_args(rtn))
and get_origin(rargs[0]) is Literal
and (vals := get_args(rargs[0]))
):
ends = vals
except (TypeError, StopIteration):
pass
if input is not None:
self._add_schema(input)
self.nodes[cast(str, node)] = StateNodeSpec(
@@ -347,6 +402,7 @@ class StateGraph(Graph):
metadata,
input=input or self.schema,
retry_policy=retry,
ends=ends,
)
return self
@@ -363,7 +419,7 @@ class StateGraph(Graph):
ValueError: If the start key is 'END' or if the start key or end key is not present in the graph.
Returns:
None
StateGraph
"""
if isinstance(start_key, str):
return super().add_edge(start_key, end_key)
@@ -386,6 +442,48 @@ class StateGraph(Graph):
self.waiting_edges.add((tuple(start_key), end_key))
return self
def add_sequence(
self,
nodes: Sequence[Union[RunnableLike, tuple[str, RunnableLike]]],
) -> Self:
"""Add a sequence of nodes that will be executed in the provided order.
Args:
nodes: A sequence of RunnableLike objects (e.g. a LangChain Runnable or a callable) or (name, RunnableLike) tuples.
If no names are provided, the name will be inferred from the node object (e.g. a runnable or a callable name).
Each node will be executed in the order provided.
Raises:
ValueError: if the sequence is empty.
ValueError: if the sequence contains duplicate node names.
Returns:
StateGraph
"""
if len(nodes) < 1:
raise ValueError("Sequence requires at least one node.")
previous_name: Optional[str] = None
for node in nodes:
if isinstance(node, tuple) and len(node) == 2:
name, node = node
else:
name = _get_node_name(node)
if name in self.nodes:
raise ValueError(
f"Node names must be unique: node with the name '{name}' already exists. "
"If you need to use two different runnables/callables with the same name (for example, using `lambda`), please provide them as tuples (name, runnable/callable)."
)
self.add_node(name, node)
if previous_name is not None:
self.add_edge(previous_name, name)
previous_name = name
return self
def compile(
self,
checkpointer: Checkpointer = None,
@@ -401,9 +499,11 @@ class StateGraph(Graph):
streamed, batched, and run asynchronously.
Args:
checkpointer (Checkpointer): An optional checkpoint saver object.
This serves as a fully versioned "memory" for the graph, allowing
the graph to be paused and resumed, and replayed from any point.
checkpointer (Optional[Union[Checkpointer, Literal[False]]]): A checkpoint saver object or flag.
If provided, this Checkpointer serves as a fully versioned "short-term memory" for the graph,
allowing it to be paused, resumed, and replayed from any point.
If None, it may inherit the parent graph's checkpointer when used as a subgraph.
If False, it will not use or inherit any checkpointer.
interrupt_before (Optional[Sequence[str]]): An optional list of node names to interrupt before.
interrupt_after (Optional[Sequence[str]]): An optional list of node names to interrupt after.
debug (bool): A flag indicating whether to enable debug mode.
@@ -468,6 +568,9 @@ class StateGraph(Graph):
for key, node in self.nodes.items():
compiled.attach_node(key, node)
for key, node in self.nodes.items():
compiled.attach_branch(key, SELF, CONTROL_BRANCH, with_reader=False)
for start, end in self.edges:
compiled.attach_edge(start, end)
@@ -518,11 +621,23 @@ class CompiledStateGraph(CompiledGraph):
if is_writable_managed_value(v)
]
def _get_root(input: Any) -> Any:
if isinstance(input, Command):
return input.update
else:
return input
def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any:
if input is None:
return SKIP_WRITE
elif isinstance(input, dict):
if all(k not in output_keys for k in input):
raise InvalidUpdateError(
f"Expected node {key} to update at least one of {output_keys}, got {input}"
)
return input.get(key, SKIP_WRITE)
elif isinstance(input, Command):
return _get_state_key(input.update, key=key)
elif get_type_hints(type(input)):
value = getattr(input, key, SKIP_WRITE)
return value if value is not None else SKIP_WRITE
@@ -535,7 +650,7 @@ class CompiledStateGraph(CompiledGraph):
# state updaters
write_entries = (
[ChannelWriteEntry("__root__", skip_none=True)]
[ChannelWriteEntry("__root__", skip_none=True, mapper=_get_root)]
if output_keys == ["__root__"]
else [
ChannelWriteEntry(key, mapper=partial(_get_state_key, key=key))
@@ -578,7 +693,6 @@ class CompiledStateGraph(CompiledGraph):
ChannelWrite(
[ChannelWriteEntry(key, key)] + write_entries,
tags=[TAG_HIDDEN],
require_at_least_one_of=output_keys,
),
],
metadata=node.metadata,
@@ -615,7 +729,9 @@ class CompiledStateGraph(CompiledGraph):
[ChannelWriteEntry(channel_name, start)], tags=[TAG_HIDDEN]
)
def attach_branch(self, start: str, name: str, branch: Branch) -> None:
def attach_branch(
self, start: str, name: str, branch: Branch, *, with_reader: bool = True
) -> None:
def branch_writer(
packets: Sequence[Union[str, Send]], config: RunnableConfig
) -> None:
@@ -648,7 +764,8 @@ class CompiledStateGraph(CompiledGraph):
else self.builder.schema
)
self.nodes[start] |= branch.run(
branch_writer, _get_state_reader(self.builder, schema)
branch_writer,
_get_state_reader(self.builder, schema) if with_reader else None,
)
# attach branch subscribers
@@ -697,6 +814,46 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
return schema(**input)
def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
if isinstance(value, Send):
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
else:
rtn.extend(value.goto)
if isinstance(value.send, Send):
rtn.append(value.send)
else:
rtn.extend(value.send)
return rtn
async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
if isinstance(value, Send):
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
else:
rtn.extend(value.goto)
if isinstance(value.send, Send):
rtn.append(value.send)
else:
rtn.extend(value.send)
return rtn
CONTROL_BRANCH_PATH = RunnableCallable(
_control_branch, _acontrol_branch, tags=[TAG_HIDDEN], trace=False, recurse=False
)
CONTROL_BRANCH = Branch(CONTROL_BRANCH_PATH, None)
def _get_channels(
schema: Type[dict],
) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec]]:
@@ -823,21 +980,3 @@ def _get_schema(
if k in channels and isinstance(channels[k], BaseChannel)
},
)
def _get_input_schema_from_type_hint(
action: Optional[RunnableLike],
) -> Optional[Type[Any]]:
if not isfunction(action) and not ismethod(getattr(action, "__call__", None)):
return None
action = cast(Callable, action)
try:
hints = get_type_hints(getattr(action, "__call__")) or get_type_hints(action)
first_parameter_name = next(iter(inspect.signature(action).parameters.keys()))
input_hint = hints.get(first_parameter_name)
if isinstance(input_hint, type) and get_type_hints(input_hint):
return input_hint
except (TypeError, StopIteration):
pass
return None
+427 -109
View File
@@ -54,6 +54,7 @@ from langgraph.checkpoint.base import (
)
from langgraph.constants import (
CONF,
CONFIG_KEY_CHECKPOINT_ID,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_CHECKPOINTER,
CONFIG_KEY_NODE_FINISHED,
@@ -64,9 +65,15 @@ from langgraph.constants import (
CONFIG_KEY_STREAM,
CONFIG_KEY_STREAM_WRITER,
CONFIG_KEY_TASK_ID,
END,
ERROR,
INPUT,
INTERRUPT,
NS_END,
NS_SEP,
NULL_TASK_ID,
PUSH,
SCHEDULED,
)
from langgraph.errors import (
ErrorCode,
@@ -95,7 +102,13 @@ from langgraph.pregel.utils import find_subgraph_pregel, get_new_channel_version
from langgraph.pregel.validate import validate_graph, validate_keys
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.store.base import BaseStore
from langgraph.types import All, Checkpointer, LoopProtocol, StateSnapshot, StreamMode
from langgraph.types import (
All,
Checkpointer,
LoopProtocol,
StateSnapshot,
StreamMode,
)
from langgraph.utils.config import (
ensure_config,
merge_configs,
@@ -439,6 +452,7 @@ class Pregel(PregelProtocol):
config: RunnableConfig,
saved: Optional[CheckpointTuple],
recurse: Optional[BaseCheckpointSaver] = None,
apply_pending_writes: bool = False,
) -> StateSnapshot:
if not saved:
return StateSnapshot(
@@ -464,12 +478,16 @@ class Pregel(PregelProtocol):
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
saved.checkpoint,
saved.pending_writes or [],
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=False,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# get the subgraphs
subgraphs = dict(self.get_subgraphs())
@@ -503,10 +521,29 @@ class Pregel(PregelProtocol):
task_states[task.id] = subgraphs[task.name].get_state(
config, subgraphs=True
)
# apply pending writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
if apply_pending_writes and saved.pending_writes:
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(saved.checkpoint, channels, tasks, None)
# assemble the state snapshot
return StateSnapshot(
read_channels(channels, self.stream_channels_asis),
tuple(t.name for t in next_tasks.values()),
tuple(t.name for t in next_tasks.values() if not t.writes),
patch_checkpoint_map(saved.config, saved.metadata),
saved.metadata,
saved.checkpoint["ts"],
@@ -524,6 +561,7 @@ class Pregel(PregelProtocol):
config: RunnableConfig,
saved: Optional[CheckpointTuple],
recurse: Optional[BaseCheckpointSaver] = None,
apply_pending_writes: bool = False,
) -> StateSnapshot:
if not saved:
return StateSnapshot(
@@ -552,12 +590,16 @@ class Pregel(PregelProtocol):
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
saved.checkpoint,
saved.pending_writes or [],
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=False,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# get the subgraphs
subgraphs = {n: g async for n, g in self.aget_subgraphs()}
@@ -591,10 +633,29 @@ class Pregel(PregelProtocol):
task_states[task.id] = await subgraphs[task.name].aget_state(
config, subgraphs=True
)
# apply pending writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
if apply_pending_writes and saved.pending_writes:
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(saved.checkpoint, channels, tasks, None)
# assemble the state snapshot
return StateSnapshot(
read_channels(channels, self.stream_channels_asis),
tuple(t.name for t in next_tasks.values()),
tuple(t.name for t in next_tasks.values() if not t.writes),
patch_checkpoint_map(saved.config, saved.metadata),
saved.metadata,
saved.checkpoint["ts"],
@@ -638,7 +699,10 @@ class Pregel(PregelProtocol):
config = merge_configs(self.config, config) if self.config else config
saved = checkpointer.get_tuple(config)
return self._prepare_state_snapshot(
config, saved, recurse=checkpointer if subgraphs else None
config,
saved,
recurse=checkpointer if subgraphs else None,
apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
)
async def aget_state(
@@ -672,7 +736,10 @@ class Pregel(PregelProtocol):
config = merge_configs(self.config, config) if self.config else config
saved = await checkpointer.aget_tuple(config)
return await self._aprepare_state_snapshot(
config, saved, recurse=checkpointer if subgraphs else None
config,
saved,
recurse=checkpointer if subgraphs else None,
apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
)
def get_state_history(
@@ -814,7 +881,7 @@ class Pregel(PregelProtocol):
raise ValueError(f"Subgraph {recast_checkpoint_ns} not found")
# get last checkpoint
config = merge_configs(self.config, config) if self.config else config
config = ensure_config(self.config, config)
saved = checkpointer.get_tuple(config)
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
checkpoint_previous_versions = (
@@ -826,62 +893,176 @@ class Pregel(PregelProtocol):
config,
{CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")},
)
checkpoint_metadata = config["metadata"]
if saved:
checkpoint_config = patch_configurable(config, saved.config[CONF])
# find last node that updated the state, if not provided
if values is None and as_node is None:
next_config = checkpointer.put(
checkpoint_config,
create_checkpoint(checkpoint, None, step),
{
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
elif as_node is None and not any(
v for vv in checkpoint["versions_seen"].values() for v in vv.values()
):
if (
isinstance(self.input_channels, str)
and self.input_channels in self.nodes
):
as_node = self.input_channels
elif as_node is None:
last_seen_by_node = sorted(
(v, n)
for n, seen in checkpoint["versions_seen"].items()
if n in self.nodes
for v in seen.values()
)
# if two nodes updated the state at the same time, it's ambiguous
if last_seen_by_node:
if len(last_seen_by_node) == 1:
as_node = last_seen_by_node[0][1]
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# update channels
checkpoint_metadata = {**saved.metadata, **checkpoint_metadata}
with ChannelsManager(
self.channels,
checkpoint,
LoopProtocol(config=config, step=step + 1, stop=step + 2),
) as (
channels,
managed,
):
) as (channels, managed):
# no values as END, just clear all tasks
if values is None and as_node == END:
if saved is not None:
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes or [],
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# apply null writes
if null_writes := [
w[1:]
for w in saved.pending_writes or []
if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
# apply writes from tasks that already ran
for tid, k, v in saved.pending_writes or []:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
# clear all current tasks
apply_writes(checkpoint, channels, next_tasks.values(), None)
# save checkpoint
next_config = checkpointer.put(
checkpoint_config,
create_checkpoint(checkpoint, None, step),
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
# no values, copy checkpoint
if values is None and as_node is None:
next_checkpoint = create_checkpoint(checkpoint, None, step)
# copy checkpoint
next_config = checkpointer.put(
checkpoint_config,
next_checkpoint,
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
if values is None and as_node == "__copy__":
next_checkpoint = create_checkpoint(checkpoint, None, step)
# copy checkpoint
next_config = checkpointer.put(
saved.parent_config or saved.config if saved else checkpoint_config,
next_checkpoint,
{
**checkpoint_metadata,
"source": "fork",
"step": step + 1,
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
# apply pending writes, if not on specific checkpoint
if (
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
and saved is not None
and saved.pending_writes
):
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes,
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# apply null writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
# apply writes
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(checkpoint, channels, tasks, None)
# find last node that updated the state, if not provided
if as_node is None and not any(
v for vv in checkpoint["versions_seen"].values() for v in vv.values()
):
if (
isinstance(self.input_channels, str)
and self.input_channels in self.nodes
):
as_node = self.input_channels
elif as_node is None:
last_seen_by_node = sorted(
(v, n)
for n, seen in checkpoint["versions_seen"].items()
if n in self.nodes
for v in seen.values()
)
# if two nodes updated the state at the same time, it's ambiguous
if last_seen_by_node:
if len(last_seen_by_node) == 1:
as_node = last_seen_by_node[0][1]
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# create task to run all writers of the chosen node
writers = self.nodes[as_node].flat_writers
if not writers:
raise InvalidUpdateError(f"Node {as_node} has no writers")
writes: deque[tuple[str, Any]] = deque()
task = PregelTaskWrites(as_node, writes, [INTERRUPT])
task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
# execute task
@@ -910,8 +1091,14 @@ class Pregel(PregelProtocol):
),
)
# save task writes
if saved:
checkpointer.put_writes(checkpoint_config, task.writes, task_id)
# channel writes are saved to current checkpoint
# push writes are saved to next checkpoint
channel_writes, push_writes = (
[w for w in task.writes if w[0] != PUSH],
[w for w in task.writes if w[0] == PUSH],
)
if saved and channel_writes:
checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
# apply to checkpoint and save
mv_writes = apply_writes(
checkpoint, channels, [task], checkpointer.get_next_version
@@ -922,6 +1109,7 @@ class Pregel(PregelProtocol):
checkpoint_config,
checkpoint,
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {as_node: values},
@@ -931,6 +1119,8 @@ class Pregel(PregelProtocol):
checkpoint_previous_versions, checkpoint["channel_versions"]
),
)
if push_writes:
checkpointer.put_writes(next_config, push_writes, task_id)
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
async def aupdate_state(
@@ -966,7 +1156,7 @@ class Pregel(PregelProtocol):
raise ValueError(f"Subgraph {recast_checkpoint_ns} not found")
# get last checkpoint
config = merge_configs(self.config, config) if self.config else config
config = ensure_config(self.config, config)
saved = await checkpointer.aget_tuple(config)
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
checkpoint_previous_versions = (
@@ -978,46 +1168,10 @@ class Pregel(PregelProtocol):
config,
{CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")},
)
checkpoint_metadata = config["metadata"]
if saved:
checkpoint_config = patch_configurable(config, saved.config[CONF])
# find last node that updated the state, if not provided
if values is None and as_node is None:
next_config = await checkpointer.aput(
checkpoint_config,
create_checkpoint(checkpoint, None, step),
{
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
elif as_node is None and not saved:
if (
isinstance(self.input_channels, str)
and self.input_channels in self.nodes
):
as_node = self.input_channels
elif as_node is None:
last_seen_by_node = sorted(
(v, n)
for n, seen in checkpoint["versions_seen"].items()
if n in self.nodes
for v in seen.values()
)
# if two nodes updated the state at the same time, it's ambiguous
if last_seen_by_node:
if len(last_seen_by_node) == 1:
as_node = last_seen_by_node[0][1]
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# update channels, acting as the chosen node
checkpoint_metadata = {**saved.metadata, **checkpoint_metadata}
async with AsyncChannelsManager(
self.channels,
checkpoint,
@@ -1026,12 +1180,164 @@ class Pregel(PregelProtocol):
channels,
managed,
):
# no values, just clear all tasks
if values is None and as_node == END:
if saved is not None:
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes or [],
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# apply null writes
if null_writes := [
w[1:]
for w in saved.pending_writes or []
if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
# apply writes from tasks that already ran
for tid, k, v in saved.pending_writes or []:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
# clear all current tasks
apply_writes(checkpoint, channels, next_tasks.values(), None)
# save checkpoint
next_config = await checkpointer.aput(
checkpoint_config,
create_checkpoint(checkpoint, None, step),
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
# no values, copy checkpoint
if values is None and as_node is None:
next_checkpoint = create_checkpoint(checkpoint, None, step)
# copy checkpoint
next_config = await checkpointer.aput(
checkpoint_config,
next_checkpoint,
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {},
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
if values is None and as_node == "__copy__":
next_checkpoint = create_checkpoint(checkpoint, None, step)
# copy checkpoint
next_config = await checkpointer.aput(
saved.parent_config or saved.config if saved else checkpoint_config,
next_checkpoint,
{
**checkpoint_metadata,
"source": "fork",
"step": step + 1,
"parents": saved.metadata.get("parents", {}) if saved else {},
},
{},
)
return patch_checkpoint_map(
next_config, saved.metadata if saved else None
)
# apply pending writes, if not on specific checkpoint
if (
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
and saved is not None
and saved.pending_writes
):
# tasks for this checkpoint
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes,
self.nodes,
channels,
managed,
saved.config,
saved.metadata.get("step", -1) + 1,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer or None,
manager=None,
)
# apply null writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
saved.checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
None,
)
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(checkpoint, channels, tasks, None)
# find last node that updated the state, if not provided
if as_node is None and not saved:
if (
isinstance(self.input_channels, str)
and self.input_channels in self.nodes
):
as_node = self.input_channels
elif as_node is None:
last_seen_by_node = sorted(
(v, n)
for n, seen in checkpoint["versions_seen"].items()
if n in self.nodes
for v in seen.values()
)
# if two nodes updated the state at the same time, it's ambiguous
if last_seen_by_node:
if len(last_seen_by_node) == 1:
as_node = last_seen_by_node[0][1]
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
as_node = last_seen_by_node[-1][1]
if as_node is None:
raise InvalidUpdateError("Ambiguous update, specify as_node")
if as_node not in self.nodes:
raise InvalidUpdateError(f"Node {as_node} does not exist")
# create task to run all writers of the chosen node
writers = self.nodes[as_node].flat_writers
if not writers:
raise InvalidUpdateError(f"Node {as_node} has no writers")
writes: deque[tuple[str, Any]] = deque()
task = PregelTaskWrites(as_node, writes, [INTERRUPT])
task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
# execute task
@@ -1060,18 +1366,28 @@ class Pregel(PregelProtocol):
),
)
# save task writes
if saved:
await checkpointer.aput_writes(checkpoint_config, writes, task_id)
# channel writes are saved to current checkpoint
# push writes are saved to next checkpoint
channel_writes, push_writes = (
[w for w in task.writes if w[0] != PUSH],
[w for w in task.writes if w[0] == PUSH],
)
if saved and channel_writes:
await checkpointer.aput_writes(
checkpoint_config, channel_writes, task_id
)
# apply to checkpoint and save
mv_writes = apply_writes(
checkpoint, channels, [task], checkpointer.get_next_version
)
assert not mv_writes, "Can't write to SharedValues from update_state"
checkpoint = create_checkpoint(checkpoint, channels, step + 1)
# save checkpoint, after applying writes
next_config = await checkpointer.aput(
checkpoint_config,
checkpoint,
{
**checkpoint_metadata,
"source": "update",
"step": step + 1,
"writes": {as_node: values},
@@ -1081,6 +1397,9 @@ class Pregel(PregelProtocol):
checkpoint_previous_versions, checkpoint["channel_versions"]
),
)
# save push writes
if push_writes:
await checkpointer.aput_writes(next_config, push_writes, task_id)
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
def _defaults(
@@ -1283,12 +1602,16 @@ class Pregel(PregelProtocol):
specs=self.channels,
output_keys=output_keys,
stream_keys=self.stream_channels_asis,
interrupt_before=interrupt_before_,
interrupt_after=interrupt_after_,
manager=run_manager,
debug=debug,
) as loop:
# create runner
runner = PregelRunner(
submit=loop.submit,
put_writes=loop.put_writes,
schedule_task=loop.accept_push,
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
)
# enable subgraph streaming
@@ -1319,12 +1642,7 @@ class Pregel(PregelProtocol):
# channel updates from step N are only visible in step N+1
# channels are guaranteed to be immutable for the duration of the step,
# with channel updates applied only at the transition between steps
while loop.tick(
input_keys=self.input_channels,
interrupt_before=interrupt_before_,
interrupt_after=interrupt_after_,
manager=run_manager,
):
while loop.tick(input_keys=self.input_channels):
for _ in runner.tick(
loop.tasks.values(),
timeout=self.step_timeout,
@@ -1505,11 +1823,16 @@ class Pregel(PregelProtocol):
specs=self.channels,
output_keys=output_keys,
stream_keys=self.stream_channels_asis,
interrupt_before=interrupt_before_,
interrupt_after=interrupt_after_,
manager=run_manager,
debug=debug,
) as loop:
# create runner
runner = PregelRunner(
submit=loop.submit,
put_writes=loop.put_writes,
schedule_task=loop.accept_push,
use_astream=do_stream is not None,
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
)
@@ -1529,12 +1852,7 @@ class Pregel(PregelProtocol):
# channel updates from step N are only visible in step N+1
# channels are guaranteed to be immutable for the duration of the step,
# with channel updates applied only at the transition between steps
while loop.tick(
input_keys=self.input_channels,
interrupt_before=interrupt_before_,
interrupt_after=interrupt_after_,
manager=run_manager,
):
while loop.tick(input_keys=self.input_channels):
async for _ in runner.atick(
loop.tasks.values(),
timeout=self.step_timeout,
+198 -44
View File
@@ -25,6 +25,7 @@ from langgraph.channels.base import BaseChannel
from langgraph.checkpoint.base import (
BaseCheckpointSaver,
Checkpoint,
PendingWrite,
V,
copy_checkpoint,
)
@@ -35,17 +36,21 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_CHECKPOINTER,
CONFIG_KEY_READ,
CONFIG_KEY_RESUME_VALUE,
CONFIG_KEY_SEND,
CONFIG_KEY_STORE,
CONFIG_KEY_TASK_ID,
EMPTY_SEQ,
INTERRUPT,
MISSING,
NO_WRITES,
NS_END,
NS_SEP,
NULL_TASK_ID,
PULL,
PUSH,
RESERVED,
RESUME,
TAG_HIDDEN,
TASKS,
Send,
@@ -67,6 +72,9 @@ class WritesProtocol(Protocol):
"""Protocol for objects containing writes to be applied to checkpoint.
Implemented by PregelTaskWrites and PregelExecutableTask."""
@property
def path(self) -> tuple[Union[str, int, tuple], ...]: ...
@property
def name(self) -> str: ...
@@ -81,6 +89,7 @@ class PregelTaskWrites(NamedTuple):
"""Simplest implementation of WritesProtocol, for usage with writes that
don't originate from a runnable task, eg. graph input, update_state, etc."""
path: tuple[Union[str, int, tuple], ...]
name: str
writes: Sequence[tuple[str, Any]]
triggers: Sequence[str]
@@ -168,7 +177,7 @@ def local_write(
"""Function injected under CONFIG_KEY_SEND in task config, to write to channels.
Validates writes and forwards them to `commit` function."""
for chan, value in writes:
if chan == TASKS:
if chan in (PUSH, TASKS):
if not isinstance(value, Send):
raise InvalidUpdateError(f"Expected Send, got {value}")
if value.node not in process_keys:
@@ -190,6 +199,14 @@ def apply_writes(
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
to the checkpoint and channels, and return managed values writes to be applied
externally."""
# sort tasks on path, to ensure deterministic order for update application
# any path parts after the 3rd are ignored for sorting
# (we use them for eg. task ids which aren't good for sorting)
tasks = sorted(tasks, key=lambda t: t.path[:3])
# if no task has triggers this is applying writes from the null task only
# so we don't do anything other than update the channels written to
bump_step = any(t.triggers for t in tasks)
# update seen versions
for task in tasks:
checkpoint["versions_seen"].setdefault(task.name, {}).update(
@@ -220,7 +237,7 @@ def apply_writes(
)
# clear pending sends
if checkpoint["pending_sends"]:
if checkpoint["pending_sends"] and bump_step:
checkpoint["pending_sends"].clear()
# Group writes by channel
@@ -228,9 +245,9 @@ def apply_writes(
pending_writes_by_managed: dict[str, list[Any]] = defaultdict(list)
for task in tasks:
for chan, val in task.writes:
if chan == NO_WRITES:
if chan in (NO_WRITES, PUSH, RESUME, INTERRUPT):
pass
elif chan == TASKS:
elif chan == TASKS: # TODO: remove branch in 1.0
checkpoint["pending_sends"].append(val)
elif chan in channels:
pending_writes_by_channel[chan].append(val)
@@ -255,13 +272,14 @@ def apply_writes(
updated_channels.add(chan)
# Channels that weren't updated in this step are notified of a new step
for chan in channels:
if chan not in updated_channels:
if channels[chan].update([]) and get_next_version is not None:
checkpoint["channel_versions"][chan] = get_next_version(
max_version,
channels[chan],
)
if bump_step:
for chan in channels:
if chan not in updated_channels:
if channels[chan].update([]) and get_next_version is not None:
checkpoint["channel_versions"][chan] = get_next_version(
max_version,
channels[chan],
)
# Return managed values writes to be applied externally
return pending_writes_by_managed
@@ -270,6 +288,7 @@ def apply_writes(
@overload
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -286,6 +305,7 @@ def prepare_next_tasks(
@overload
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -301,6 +321,7 @@ def prepare_next_tasks(
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -315,13 +336,14 @@ def prepare_next_tasks(
"""Prepare the set of tasks that will make up the next Pregel step.
This is the union of all PUSH tasks (Sends) and PULL tasks (nodes triggered
by edges)."""
tasks: dict[str, Union[PregelTask, PregelExecutableTask]] = {}
# Consume pending packets
for idx, _ in enumerate(checkpoint["pending_sends"]):
tasks: list[Union[PregelTask, PregelExecutableTask]] = []
# Consume pending_sends from previous step (legacy version of Send)
for idx, _ in enumerate(checkpoint["pending_sends"]): # TODO: remove branch in 1.0
if task := prepare_single_task(
(PUSH, idx),
None,
checkpoint=checkpoint,
pending_writes=pending_writes,
processes=processes,
channels=channels,
managed=managed,
@@ -332,7 +354,7 @@ def prepare_next_tasks(
checkpointer=checkpointer,
manager=manager,
):
tasks[task.id] = task
tasks.append(task)
# Check if any processes should be run in next step
# If so, prepare the values to be passed to them
for name in processes:
@@ -340,6 +362,7 @@ def prepare_next_tasks(
(PULL, name),
None,
checkpoint=checkpoint,
pending_writes=pending_writes,
processes=processes,
channels=channels,
managed=managed,
@@ -350,15 +373,74 @@ def prepare_next_tasks(
checkpointer=checkpointer,
manager=manager,
):
tasks[task.id] = task
return tasks
tasks.append(task)
# Consume pending Sends from this step (new version of Send)
if any(c == PUSH for _, c, _ in pending_writes):
# group writes by task id
grouped_by_task = defaultdict(list)
for tid, c, _ in pending_writes:
grouped_by_task[tid].append(c)
# prepare send tasks from grouped writes
# 1. start from sends originating from existing tasks
tidx = 0
while tidx < len(tasks):
task = tasks[tidx]
if twrites := grouped_by_task.pop(task.id, None):
for idx, c in enumerate(twrites):
if c != PUSH:
continue
if next_task := prepare_single_task(
(PUSH, task.path, idx, task.id),
None,
checkpoint=checkpoint,
pending_writes=pending_writes,
processes=processes,
channels=channels,
managed=managed,
config=config,
step=step,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
):
tasks.append(next_task)
tidx += 1
# key tasks by id
task_map = {t.id: t for t in tasks}
# 2. create new tasks for remaining sends (eg. from update_state)
for tid, writes in grouped_by_task.items():
task = task_map.get(tid)
for idx, c in enumerate(writes):
if c != PUSH:
continue
if next_task := prepare_single_task(
(PUSH, task.path if task else (), idx, tid),
None,
checkpoint=checkpoint,
pending_writes=pending_writes,
processes=processes,
channels=channels,
managed=managed,
config=config,
step=step,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
):
task_map[next_task.id] = next_task
else:
task_map = {t.id: t for t in tasks}
return task_map
def prepare_single_task(
task_path: tuple[str, Union[int, str]],
task_path: tuple[Union[str, int, tuple], ...],
task_id_checksum: Optional[str],
*,
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -376,31 +458,74 @@ def prepare_single_task(
parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
if task_path[0] == PUSH:
idx = int(task_path[1])
if idx >= len(checkpoint["pending_sends"]):
return
packet = checkpoint["pending_sends"][idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
if len(task_path) == 2: # TODO: remove branch in 1.0
# legacy SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if idx >= len(checkpoint["pending_sends"]):
return
packet = checkpoint["pending_sends"][idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(
f"Ignoring unknown node name {packet.node} in pending sends"
)
return
# create task id
triggers = [PUSH]
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = _uuid5_str(
checkpoint_id,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
elif len(task_path) == 4:
# new PUSH tasks, executed in superstep n
# (PUSH, parent task path, idx of PUSH write, id of parent task)
task_path_t = cast(tuple[str, tuple, int, str], task_path)
writes_for_path = [w for w in pending_writes if w[0] == task_path_t[3]]
if task_path_t[2] >= len(writes_for_path):
logger.warning(
f"Ignoring invalid write index {task_path[2]} in pending writes"
)
return
packet = writes_for_path[task_path_t[2]][2]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending writes"
)
return
if packet.node not in processes:
logger.warning(
f"Ignoring unknown node name {packet.node} in pending writes"
)
return
# create task id
triggers = [PUSH]
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = _uuid5_str(
checkpoint_id,
checkpoint_ns,
str(step),
packet.node,
PUSH,
_tuple_str(task_path[1]),
str(task_path[2]),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
if packet.node not in processes:
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
return
# create task id
triggers = [PUSH]
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = _uuid5_str(
checkpoint_id,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
metadata = {
"langgraph_step": step,
@@ -410,7 +535,7 @@ def prepare_single_task(
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
proc = processes[packet.node]
if node := proc.node:
@@ -444,7 +569,9 @@ def prepare_single_task(
checkpoint,
channels,
managed,
PregelTaskWrites(packet.node, writes, triggers),
PregelTaskWrites(
task_path, packet.node, writes, triggers
),
config,
),
CONFIG_KEY_STORE: (
@@ -460,6 +587,14 @@ def prepare_single_task(
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_RESUME_VALUE: next(
(
v
for tid, c, v in pending_writes
if tid in (NULL_TASK_ID, task_id) and c == RESUME
),
MISSING,
),
},
),
triggers,
@@ -472,6 +607,7 @@ def prepare_single_task(
else:
return PregelTask(task_id, packet.node, task_path)
elif task_path[0] == PULL:
# (PULL, node name)
name = cast(str, task_path[1])
if name not in processes:
return
@@ -552,7 +688,7 @@ def prepare_single_task(
checkpoint,
channels,
managed,
PregelTaskWrites(name, writes, triggers),
PregelTaskWrites(task_path, name, writes, triggers),
config,
),
CONFIG_KEY_STORE: (
@@ -568,6 +704,15 @@ def prepare_single_task(
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_RESUME_VALUE: next(
(
v
for tid, c, v in pending_writes
if tid in (NULL_TASK_ID, task_id)
and c == RESUME
),
MISSING,
),
},
),
triggers,
@@ -633,3 +778,12 @@ def _uuid5_str(namespace: bytes, *parts: str) -> str:
sha.update(b"".join(p.encode() for p in parts))
hex = sha.hexdigest()
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
def _tuple_str(tup: Union[str, int, tuple]) -> str:
"""Generate a string representation of a tuple."""
return (
f"({', '.join(_tuple_str(x) for x in tup)})"
if isinstance(tup, (tuple, list))
else str(tup)
)
+1 -1
View File
@@ -208,7 +208,7 @@ def print_step_tasks(step: int, next_tasks: list[PregelExecutableTask]) -> None:
print(
f"{get_colored_text(f'[{step}:tasks]', color='blue')} "
+ get_bolded_text(
f"Starting step {step} with {n_tasks} task{'s' if n_tasks != 1 else ''}:\n"
f"Starting {n_tasks} task{'s' if n_tasks != 1 else ''} for step {step}:\n"
)
+ "\n".join(
f"- {get_colored_text(task.name, 'green')} -> {pformat(task.input)}"
+52 -2
View File
@@ -1,11 +1,31 @@
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypeVar, Union
from uuid import UUID
from langchain_core.runnables.utils import AddableDict
from langgraph.channels.base import BaseChannel, EmptyChannelError
from langgraph.constants import EMPTY_SEQ, ERROR, INTERRUPT, TAG_HIDDEN
from langgraph.constants import (
EMPTY_SEQ,
ERROR,
FF_SEND_V2,
INTERRUPT,
NULL_TASK_ID,
PUSH,
RESUME,
TAG_HIDDEN,
TASKS,
)
from langgraph.pregel.log import logger
from langgraph.types import PregelExecutableTask
from langgraph.types import Command, PregelExecutableTask, Send
def is_task_id(task_id: str) -> bool:
"""Check if a string is a valid task id."""
try:
UUID(task_id)
except ValueError:
return False
return True
def read_channel(
@@ -44,6 +64,36 @@ def read_channels(
return values
def map_command(
cmd: Command,
) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.send:
if isinstance(cmd.send, (tuple, list)):
sends = cmd.send
else:
sends = [cmd.send]
for send in sends:
if not isinstance(send, Send):
raise TypeError(
f"In Command.send, expected Send, got {type(send).__name__}"
)
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
yield (tid, RESUME, resume)
else:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
if not isinstance(cmd.update, dict):
raise TypeError(
f"Expected cmd.update to be a dict mapping channel names to update values, got {type(cmd.update).__name__}"
)
for k, v in cmd.update.items():
yield (NULL_TASK_ID, k, v)
def map_input(
input_channels: Union[str, Sequence[str]],
chunk: Optional[Union[dict[str, Any], Any]],
+174 -35
View File
@@ -1,6 +1,6 @@
import asyncio
import concurrent.futures
from collections import deque
from collections import defaultdict, deque
from contextlib import AsyncExitStack, ExitStack
from types import TracebackType
from typing import (
@@ -52,6 +52,9 @@ from langgraph.constants import (
INPUT,
INTERRUPT,
NS_SEP,
NULL_TASK_ID,
PUSH,
RESUME,
SCHEDULED,
TAG_HIDDEN,
)
@@ -74,6 +77,7 @@ from langgraph.pregel.algo import (
apply_writes,
increment,
prepare_next_tasks,
prepare_single_task,
should_interrupt,
)
from langgraph.pregel.debug import (
@@ -90,6 +94,7 @@ from langgraph.pregel.executor import (
Submit,
)
from langgraph.pregel.io import (
map_command,
map_input,
map_output_updates,
map_output_values,
@@ -100,7 +105,13 @@ from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
from langgraph.pregel.read import PregelNode
from langgraph.pregel.utils import get_new_channel_versions
from langgraph.store.base import BaseStore
from langgraph.types import All, LoopProtocol, PregelExecutableTask, StreamProtocol
from langgraph.types import (
All,
Command,
LoopProtocol,
PregelExecutableTask,
StreamProtocol,
)
from langgraph.utils.config import patch_configurable
V = TypeVar("V")
@@ -130,6 +141,9 @@ class PregelLoop(LoopProtocol):
stream_keys: Union[str, Sequence[str]]
skip_done_tasks: bool
is_nested: bool
manager: Union[None, AsyncParentRunManager, ParentRunManager]
interrupt_after: Union[All, Sequence[str]]
interrupt_before: Union[All, Sequence[str]]
checkpointer_get_next_version: GetNextVersion
checkpointer_put_writes: Optional[
@@ -162,6 +176,7 @@ class PregelLoop(LoopProtocol):
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
]
tasks: dict[str, PregelExecutableTask]
to_interrupt: list[PregelExecutableTask]
output: Union[None, dict[str, Any], Any] = None
# public
@@ -178,6 +193,9 @@ class PregelLoop(LoopProtocol):
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
output_keys: Union[str, Sequence[str]],
stream_keys: Union[str, Sequence[str]],
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
check_subgraphs: bool = True,
debug: bool = False,
) -> None:
@@ -194,6 +212,9 @@ class PregelLoop(LoopProtocol):
self.specs = specs
self.output_keys = output_keys
self.stream_keys = stream_keys
self.interrupt_after = interrupt_after
self.interrupt_before = interrupt_before
self.manager = manager
self.is_nested = CONFIG_KEY_TASK_ID in self.config.get(CONF, {})
self.skip_done_tasks = (
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
@@ -261,15 +282,60 @@ class PregelLoop(LoopProtocol):
task_id,
)
# output writes
self._output_writes(task_id, writes)
if hasattr(self, "tasks"):
self._output_writes(task_id, writes)
def accept_push(
self, task: PregelExecutableTask, write_idx: int
) -> Optional[PregelExecutableTask]:
"""Accept a PUSH from a task, potentially returning a new task to start."""
# don't start if an earlier PUSH has already triggered an interrupt
if self.to_interrupt:
return
# don't start if we should interrupt *after* the original task
if should_interrupt(self.checkpoint, self.interrupt_after, [task]):
self.to_interrupt.append(task)
return
if pushed := cast(
Optional[PregelExecutableTask],
prepare_single_task(
(PUSH, task.path, write_idx, task.id),
None,
checkpoint=self.checkpoint,
pending_writes=[(task.id, *w) for w in task.writes],
processes=self.nodes,
channels=self.channels,
managed=self.managed,
config=self.config,
step=self.step,
for_execution=True,
store=self.store,
checkpointer=self.checkpointer,
manager=self.manager,
),
):
# don't start if we should interrupt *before* the new task
if should_interrupt(self.checkpoint, self.interrupt_before, [pushed]):
self.to_interrupt.append(pushed)
return
# produce debug output
self._emit("debug", map_debug_tasks, self.step, [pushed])
# debug flag
if self.debug:
print_step_tasks(self.step, [pushed])
# save the new task
self.tasks[pushed.id] = pushed
# match any pending writes to the new task
if self.skip_done_tasks:
self._match_writes({pushed.id: pushed})
# return the new task, to be started, if not run before
if not pushed.writes:
return pushed
def tick(
self,
*,
input_keys: Union[str, Sequence[str]],
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
) -> bool:
"""Execute a single iteration of the Pregel loop.
Returns True if more iterations are needed."""
@@ -278,6 +344,10 @@ class PregelLoop(LoopProtocol):
if self.input not in (INPUT_DONE, INPUT_RESUMING):
self._first(input_keys=input_keys)
elif self.to_interrupt:
# if we need to interrupt, do so
self.status = "interrupt_before"
raise GraphInterrupt()
elif all(task.writes for task in self.tasks.values()):
writes = [w for t in self.tasks.values() for w in t.writes]
# debug flag
@@ -307,6 +377,8 @@ class PregelLoop(LoopProtocol):
)
# clear pending writes
self.checkpoint_pending_writes.clear()
# "not skip_done_tasks" only applies to first tick after resuming
self.skip_done_tasks = True
# save checkpoint
self._put_checkpoint(
{
@@ -320,7 +392,9 @@ class PregelLoop(LoopProtocol):
}
)
# after execution, check if we should interrupt
if should_interrupt(self.checkpoint, interrupt_after, self.tasks.values()):
if should_interrupt(
self.checkpoint, self.interrupt_after, self.tasks.values()
):
self.status = "interrupt_after"
raise GraphInterrupt()
else:
@@ -331,19 +405,33 @@ class PregelLoop(LoopProtocol):
self.status = "out_of_steps"
return False
# apply NULL writes
if null_writes := [
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
]:
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
# prepare next tasks
self.tasks = prepare_next_tasks(
self.checkpoint,
self.checkpoint_pending_writes,
self.nodes,
self.channels,
self.managed,
self.config,
self.step,
for_execution=True,
manager=manager,
manager=self.manager,
store=self.store,
checkpointer=self.checkpointer,
)
self.to_interrupt = []
# produce debug output
if self._checkpointer_put_after_previous is not None:
@@ -381,36 +469,16 @@ class PregelLoop(LoopProtocol):
# if there are pending writes from a previous loop, apply them
if self.skip_done_tasks and self.checkpoint_pending_writes:
for tid, k, v in self.checkpoint_pending_writes:
if k in (ERROR, INTERRUPT):
continue
if task := self.tasks.get(tid):
if k == SCHEDULED:
if v == max(
self.checkpoint["versions_seen"]
.get(INTERRUPT, {})
.values(),
default=None,
):
self.tasks[tid] = task._replace(scheduled=True)
else:
task.writes.append((k, v))
# print output for any tasks we applied previous writes to
for task in self.tasks.values():
if task.writes:
self._output_writes(task.id, task.writes, cached=True)
self._match_writes(self.tasks)
# if all tasks have finished, re-tick
if all(task.writes for task in self.tasks.values()):
return self.tick(
input_keys=input_keys,
interrupt_after=interrupt_after,
interrupt_before=interrupt_before,
manager=manager,
)
return self.tick(input_keys=input_keys)
# before execution, check if we should interrupt
if should_interrupt(self.checkpoint, interrupt_before, self.tasks.values()):
if should_interrupt(
self.checkpoint, self.interrupt_before, self.tasks.values()
):
self.status = "interrupt_before"
raise GraphInterrupt()
@@ -421,10 +489,29 @@ class PregelLoop(LoopProtocol):
if self.debug:
print_step_tasks(self.step, list(self.tasks.values()))
# print output for any tasks we applied previous writes to
for task in self.tasks.values():
if task.writes:
self._output_writes(task.id, task.writes, cached=True)
return True
# private
def _match_writes(self, tasks: Mapping[str, PregelExecutableTask]) -> None:
for tid, k, v in self.checkpoint_pending_writes:
if k in (ERROR, INTERRUPT, RESUME):
continue
if task := tasks.get(tid):
if k == SCHEDULED:
if v == max(
self.checkpoint["versions_seen"].get(INTERRUPT, {}).values(),
default=None,
):
self.tasks[tid] = task._replace(scheduled=True)
else:
task.writes.append((k, v))
def _first(self, *, input_keys: Union[str, Sequence[str]]) -> None:
# resuming from previous checkpoint requires
# - finding a previous checkpoint
@@ -445,8 +532,20 @@ class PregelLoop(LoopProtocol):
self._emit(
"values", map_output_values, self.output_keys, True, self.channels
)
# map command to writes
elif isinstance(self.input, Command):
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
# group writes by task ID
for tid, c, v in map_command(self.input):
writes[tid].append((c, v))
if not writes:
raise EmptyInputError("Received empty Command input")
# save writes
for tid, ws in writes.items():
self.put_writes(tid, ws)
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# TODO shouldn't these writes be passed to put_writes too?
# check if we should delegate (used by subgraphs in distributed mode)
if self.config[CONF].get(CONFIG_KEY_DELEGATE):
raise GraphDelegate(
@@ -460,6 +559,7 @@ class PregelLoop(LoopProtocol):
# discard any unfinished tasks from previous checkpoint
discard_tasks = prepare_next_tasks(
self.checkpoint,
self.checkpoint_pending_writes,
self.nodes,
self.channels,
self.managed,
@@ -474,7 +574,10 @@ class PregelLoop(LoopProtocol):
mv_writes = apply_writes(
self.checkpoint,
self.channels,
[*discard_tasks.values(), PregelTaskWrites(INPUT, input_writes, [])],
[
*discard_tasks.values(),
PregelTaskWrites((), INPUT, input_writes, []),
],
self.checkpointer_get_next_version,
)
assert not mv_writes, "Can't write to SharedValues in graph input"
@@ -491,6 +594,8 @@ class PregelLoop(LoopProtocol):
)
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
for k, v in self.config["metadata"].items():
metadata.setdefault(k, v) # type: ignore
# assign step and parents
metadata["step"] = self.step
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
@@ -568,11 +673,33 @@ class PregelLoop(LoopProtocol):
# save final output
self.output = read_channels(self.channels, self.output_keys)
if suppress:
# suppress interrupt
# emit one last "values" event, with pending writes applied
if (
hasattr(self, "tasks")
and self.checkpoint_pending_writes
and any(task.writes for task in self.tasks.values())
):
mv_writes = apply_writes(
self.checkpoint,
self.channels,
self.tasks.values(),
self.checkpointer_get_next_version,
)
for key, values in mv_writes.items():
self._update_mv(key, values)
self._emit(
"values",
map_output_values,
self.output_keys,
[w for t in self.tasks.values() for w in t.writes],
self.channels,
)
# emit INTERRUPT event
self._emit(
"updates",
lambda: iter([{INTERRUPT: cast(GraphInterrupt, exc_value).args[0]}]),
)
# suppress interrupt
return True
def _emit(
@@ -626,6 +753,9 @@ class SyncPregelLoop(PregelLoop, ContextManager):
checkpointer: Optional[BaseCheckpointSaver],
nodes: Mapping[str, PregelNode],
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
check_subgraphs: bool = True,
@@ -641,7 +771,10 @@ class SyncPregelLoop(PregelLoop, ContextManager):
specs=specs,
output_keys=output_keys,
stream_keys=stream_keys,
interrupt_after=interrupt_after,
interrupt_before=interrupt_before,
check_subgraphs=check_subgraphs,
manager=manager,
debug=debug,
)
self.stack = ExitStack()
@@ -752,6 +885,9 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
checkpointer: Optional[BaseCheckpointSaver],
nodes: Mapping[str, PregelNode],
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
check_subgraphs: bool = True,
@@ -767,7 +903,10 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
specs=specs,
output_keys=output_keys,
stream_keys=stream_keys,
interrupt_after=interrupt_after,
interrupt_before=interrupt_before,
check_subgraphs=check_subgraphs,
manager=manager,
debug=debug,
)
self.stack = AsyncExitStack()
+18 -2
View File
@@ -2,9 +2,15 @@ import asyncio
import logging
import random
import time
from typing import Optional, Sequence
from functools import partial
from typing import Any, Callable, Optional, Sequence
from langgraph.constants import CONF, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_RESUMING
from langgraph.constants import (
CONF,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SEND,
)
from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphInterrupt
from langgraph.types import PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
@@ -15,12 +21,17 @@ logger = logging.getLogger(__name__)
def run_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
writer: Optional[
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
] = None,
) -> None:
"""Run a task with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
attempts = 0
config = task.config
if writer is not None:
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
while True:
try:
# clear any writes from previous attempts
@@ -84,12 +95,17 @@ async def arun_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
stream: bool = False,
writer: Optional[
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
] = None,
) -> None:
"""Run a task asynchronously with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
attempts = 0
config = task.config
if writer is not None:
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
while True:
try:
# clear any writes from previous attempts
+115 -27
View File
@@ -14,7 +14,15 @@ from typing import (
cast,
)
from langgraph.constants import ERROR, INTERRUPT, NO_WRITES, TAG_HIDDEN
from langgraph.constants import (
CONF,
CONFIG_KEY_SEND,
ERROR,
INTERRUPT,
NO_WRITES,
PUSH,
TAG_HIDDEN,
)
from langgraph.errors import GraphDelegate, GraphInterrupt
from langgraph.pregel.executor import Submit
from langgraph.pregel.retry import arun_with_retry, run_with_retry
@@ -31,6 +39,9 @@ class PregelRunner:
*,
submit: Submit,
put_writes: Callable[[str, Sequence[tuple[str, Any]]], None],
schedule_task: Callable[
[PregelExecutableTask, int], Optional[PregelExecutableTask]
],
use_astream: bool = False,
node_finished: Optional[Callable[[str], None]] = None,
) -> None:
@@ -38,6 +49,7 @@ class PregelRunner:
self.put_writes = put_writes
self.use_astream = use_astream
self.node_finished = node_finished
self.schedule_task = schedule_task
def tick(
self,
@@ -48,27 +60,58 @@ class PregelRunner:
retry_policy: Optional[RetryPolicy] = None,
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
) -> Iterator[None]:
def writer(
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
) -> None:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
for idx, w in enumerate(task.writes):
# find the index for the newly inserted writes
if idx < prev_length:
continue
assert writes[idx - prev_length] is w
# bail if not a PUSH write
if w[0] != PUSH:
continue
# schedule the next task, if the callback returns one
if next_task := self.schedule_task(task, idx):
# if the parent task was retried,
# the next task might already be running
if any(
t == next_task.id for t in futures.values() if t is not None
):
continue
# schedule the next task
futures[
self.submit(
run_with_retry,
next_task,
retry_policy,
writer=writer,
__reraise_on_exit__=reraise,
)
] = next_task
tasks = tuple(tasks)
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {}
# give control back to the caller
yield
# fast path if single task with no timeout and no waiter
if len(tasks) == 1 and timeout is None and get_waiter is None:
t = tasks[0]
try:
run_with_retry(t, retry_policy)
run_with_retry(t, retry_policy, writer=writer)
self.commit(t, None)
except Exception as exc:
self.commit(t, exc)
if reraise:
raise
return
if not futures: # maybe `t` schuduled another task
return
# add waiter task if requested
if get_waiter is not None:
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {
get_waiter(): None
}
else:
futures = {}
futures[get_waiter()] = None
# execute tasks, and wait for one to fail or all to finish.
# each task is independent from all other concurrent tasks
# yield updates/debug output as each task finishes
@@ -79,10 +122,11 @@ class PregelRunner:
run_with_retry,
t,
retry_policy,
writer=writer,
__reraise_on_exit__=reraise,
)
] = t
all_futures = futures.copy()
done_futures: set[concurrent.futures.Future] = set()
end_time = timeout + time.monotonic() if timeout else None
while len(futures) > (1 if get_waiter is not None else 0):
done, inflight = concurrent.futures.wait(
@@ -99,6 +143,8 @@ class PregelRunner:
if inflight and get_waiter is not None:
futures[get_waiter()] = None
else:
# store for panic check
done_futures.add(fut)
# task finished, commit writes
self.commit(task, _exception(fut))
else:
@@ -110,7 +156,10 @@ class PregelRunner:
# give control back to the caller
yield
# panic on failure or timeout
_panic_or_proceed(all_futures, panic=reraise)
_panic_or_proceed(
done_futures.union(f for f, t in futures.items() if t is not None),
panic=reraise,
)
async def atick(
self,
@@ -121,28 +170,67 @@ class PregelRunner:
retry_policy: Optional[RetryPolicy] = None,
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
) -> AsyncIterator[None]:
def writer(
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
) -> None:
prev_length = len(task.writes)
# delegate to the underlying writer
task.config[CONF][CONFIG_KEY_SEND](writes)
for idx, w in enumerate(task.writes):
# find the index for the newly inserted writes
if idx < prev_length:
continue
assert writes[idx - prev_length] is w
# bail if not a PUSH write
if w[0] != PUSH:
continue
# schedule the next task, if the callback returns one
if next_task := self.schedule_task(task, idx):
# if the parent task was retried,
# the next task might already be running
if any(
t == next_task.id for t in futures.values() if t is not None
):
continue
# schedule the next task
futures[
cast(
asyncio.Future,
self.submit(
arun_with_retry,
next_task,
retry_policy,
stream=self.use_astream,
writer=writer,
__name__=t.name,
__cancel_on_exit__=True,
__reraise_on_exit__=reraise,
),
)
] = next_task
loop = asyncio.get_event_loop()
tasks = tuple(tasks)
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {}
# give control back to the caller
yield
# fast path if single task with no waiter and no timeout
if len(tasks) == 1 and get_waiter is None and timeout is None:
t = tasks[0]
try:
await arun_with_retry(t, retry_policy, stream=self.use_astream)
await arun_with_retry(
t, retry_policy, stream=self.use_astream, writer=writer
)
self.commit(t, None)
except Exception as exc:
self.commit(t, exc)
if reraise:
raise
return
if not futures: # maybe `t` schuduled another task
return
# add waiter task if requested
if get_waiter is not None:
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {
get_waiter(): None
}
else:
futures = {}
futures[get_waiter()] = None
# execute tasks, and wait for one to fail or all to finish.
# each task is independent from all other concurrent tasks
# yield updates/debug output as each task finishes
@@ -156,13 +244,14 @@ class PregelRunner:
t,
retry_policy,
stream=self.use_astream,
writer=writer,
__name__=t.name,
__cancel_on_exit__=True,
__reraise_on_exit__=reraise,
),
)
] = t
all_futures = futures.copy()
done_futures: set[asyncio.Future] = set()
end_time = timeout + loop.time() if timeout else None
while len(futures) > (1 if get_waiter is not None else 0):
done, inflight = await asyncio.wait(
@@ -179,6 +268,8 @@ class PregelRunner:
if inflight and get_waiter is not None:
futures[get_waiter()] = None
else:
# store for panic check
done_futures.add(fut)
# task finished, commit writes
self.commit(task, _exception(fut))
else:
@@ -194,7 +285,9 @@ class PregelRunner:
fut.cancel()
# panic on failure or timeout
_panic_or_proceed(
all_futures, timeout_exc_cls=asyncio.TimeoutError, panic=reraise
done_futures.union(f for f, t in futures.items() if t is not None),
timeout_exc_cls=asyncio.TimeoutError,
panic=reraise,
)
def commit(
@@ -250,10 +343,7 @@ def _exception(
def _panic_or_proceed(
futs: Union[
dict[concurrent.futures.Future, Optional[PregelExecutableTask]],
dict[asyncio.Future, Optional[PregelExecutableTask]],
],
futs: Union[set[concurrent.futures.Future], set[asyncio.Future]],
*,
timeout_exc_cls: Type[Exception] = TimeoutError,
panic: bool = True,
@@ -261,10 +351,8 @@ def _panic_or_proceed(
"""Cancel remaining tasks if any failed, re-raise exception if panic is True."""
done: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
for fut, val in futs.items():
if val is None:
continue
elif fut.done():
for fut in futs:
if fut.done():
done.add(fut)
else:
inflight.add(fut)
+7 -3
View File
@@ -14,7 +14,7 @@ from typing import (
from langchain_core.runnables import Runnable, RunnableConfig
from langchain_core.runnables.utils import ConfigurableFieldSpec
from langgraph.constants import CONF, CONFIG_KEY_SEND, TASKS, Send
from langgraph.constants import CONF, CONFIG_KEY_SEND, FF_SEND_V2, PUSH, TASKS, Send
from langgraph.errors import InvalidUpdateError
from langgraph.utils.runnable import RunnableCallable
@@ -112,14 +112,18 @@ class ChannelWrite(RunnableCallable):
# validate
for w in writes:
if isinstance(w, ChannelWriteEntry):
if w.channel == TASKS:
if w.channel in (TASKS, PUSH):
raise InvalidUpdateError(
"Cannot write to the reserved channel TASKS"
)
if w.value is PASSTHROUGH:
raise InvalidUpdateError("PASSTHROUGH value must be replaced")
# split packets and entries
sends = [(TASKS, packet) for packet in writes if isinstance(packet, Send)]
sends = [
(PUSH if FF_SEND_V2 else TASKS, packet)
for packet in writes
if isinstance(packet, Send)
]
entries = [write for write in writes if isinstance(write, ChannelWriteEntry)]
# process entries into values
values = [
+61 -4
View File
@@ -1,14 +1,18 @@
import dataclasses
import sys
from collections import deque
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Any,
Callable,
Generic,
Hashable,
Literal,
NamedTuple,
Optional,
Sequence,
Type,
TypeVar,
Union,
cast,
)
@@ -44,6 +48,11 @@ StreamWriter = Callable[[Any], None]
Always injected into nodes if requested as a keyword argument, but it's a no-op
when not using stream_mode="custom"."""
if sys.version_info >= (3, 10):
_DC_KWARGS = {"kw_only": True, "slots": True, "frozen": True}
else:
_DC_KWARGS = {"frozen": True}
def default_retry_on(exc: Exception) -> bool:
import httpx
@@ -101,16 +110,18 @@ class CachePolicy(NamedTuple):
pass
@dataclass
@dataclasses.dataclass(**_DC_KWARGS)
class Interrupt:
value: Any
resumable: bool = False
ns: Optional[Sequence[str]] = None
when: Literal["during"] = "during"
class PregelTask(NamedTuple):
id: str
name: str
path: tuple[Union[str, int], ...]
path: tuple[Union[str, int, tuple], ...]
error: Optional[Exception] = None
interrupts: tuple[Interrupt, ...] = ()
state: Union[None, RunnableConfig, "StateSnapshot"] = None
@@ -127,7 +138,7 @@ class PregelExecutableTask(NamedTuple):
retry_policy: Optional[RetryPolicy]
cache_policy: Optional[CachePolicy]
id: str
path: tuple[Union[str, int], ...]
path: tuple[Union[str, int, tuple], ...]
scheduled: bool = False
@@ -221,6 +232,27 @@ class Send:
)
N = TypeVar("N", bound=Hashable)
@dataclasses.dataclass(**_DC_KWARGS)
class Command(Generic[N]):
"""One or more commands to update the graph's state and send messages to nodes."""
update: Optional[dict[str, Any]] = None
send: Union[Send, Sequence[Send]] = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
def __repr__(self) -> str:
# get all non-None values
contents = ", ".join(
f"{key}={value!r}"
for key, value in dataclasses.asdict(self).items()
if value
)
return f"Command({contents})"
StreamChunk = tuple[tuple[str, ...], str, Any]
@@ -261,3 +293,28 @@ class LoopProtocol:
self.store = store
self.step = step
self.stop = stop
def interrupt(value: Any) -> Any:
from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_RESUME_VALUE,
MISSING,
NS_SEP,
)
from langgraph.errors import GraphInterrupt
from langgraph.utils.config import get_configurable
conf = get_configurable()
if (resume := conf.get(CONFIG_KEY_RESUME_VALUE, MISSING)) and resume is not MISSING:
return resume
else:
raise GraphInterrupt(
(
Interrupt(
value=value,
resumable=True,
ns=cast(str, conf[CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP),
),
)
)
+17
View File
@@ -1,3 +1,5 @@
import asyncio
import sys
from collections import ChainMap
from typing import Any, Optional, Sequence
@@ -290,3 +292,18 @@ def ensure_config(*configs: Optional[RunnableConfig]) -> RunnableConfig:
):
empty["metadata"][key] = value
return empty
def get_configurable() -> dict[str, Any]:
if sys.version_info < (3, 11):
try:
if asyncio.current_task():
raise RuntimeError(
"Python 3.11 or later required to use this in an async context"
)
except RuntimeError:
pass
if var_config := var_child_runnable_config.get():
return var_config[CONF]
else:
raise RuntimeError("Called get_configurable outside of a runnable context")
+3 -3
View File
@@ -1359,7 +1359,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.2"
version = "2.0.4"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1429,7 +1429,7 @@ url = "../checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
version = "0.1.35"
version = "0.1.36"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3425,4 +3425,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.0,<4.0"
content-hash = "f0b7800f90041227ba4d7b72482c70fabdbacc95e8a3a784fdebc88721728a91"
content-hash = "9bf5668d3f70f3b77457906732404a6401583a5966f70a72ef10a68f2a5b27ad"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.44"
version = "0.2.50"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
[tool.poetry.dependencies]
python = ">=3.9.0,<4.0"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langgraph-checkpoint = "^2.0.0"
langgraph-checkpoint = "^2.0.4"
langgraph-sdk = "^0.1.32"
[tool.poetry.group.dev.dependencies]
@@ -840,65 +840,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -926,46 +877,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -977,16 +904,10 @@
# name: test_conditional_graph[duckdb].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -1194,65 +1115,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -1280,46 +1152,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -1331,16 +1179,10 @@
# name: test_conditional_graph[memory].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -1548,65 +1390,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -1634,46 +1427,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -1685,16 +1454,10 @@
# name: test_conditional_graph[postgres].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -1902,65 +1665,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -1988,46 +1702,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -2039,16 +1729,10 @@
# name: test_conditional_graph[postgres_pipe].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -2256,65 +1940,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -2342,46 +1977,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -2393,16 +2004,10 @@
# name: test_conditional_graph[postgres_pool].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -2610,65 +2215,16 @@
"data": "__start__"
},
{
"id": 1,
"type": "schema",
"data": "Parallel<agent_outcome>Input"
},
{
"id": 2,
"type": "schema",
"data": "Parallel<agent_outcome>Output"
},
{
"id": 3,
"type": "runnable",
"data": {
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate"
],
"name": "PromptTemplate"
}
},
{
"id": 4,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"language_models",
"fake",
"FakeStreamingListLLM"
],
"name": "FakeStreamingListLLM"
}
},
{
"id": 5,
"type": "runnable",
"data": {
"id": [
"langchain_core",
"runnables",
"base",
"RunnableLambda"
],
"name": "agent_parser"
}
},
{
"id": 6,
"id": "agent",
"type": "runnable",
"data": {
"id": [
"langchain",
"schema",
"runnable",
"RunnablePassthrough"
"RunnableAssign"
],
"name": "Passthrough"
"name": "agent"
}
},
{
@@ -2696,46 +2252,22 @@
}
],
"edges": [
{
"source": 3,
"target": 4
},
{
"source": 4,
"target": 5
},
{
"source": 1,
"target": 3
},
{
"source": 5,
"target": 2
},
{
"source": 1,
"target": 6
},
{
"source": 6,
"target": 2
},
{
"source": "__start__",
"target": 1
"target": "agent"
},
{
"source": "tools",
"target": 1
"target": "agent"
},
{
"source": 2,
"source": "agent",
"target": "tools",
"data": "continue",
"conditional": true
},
{
"source": 2,
"source": "agent",
"target": "__end__",
"data": "exit",
"conditional": true
@@ -2747,16 +2279,10 @@
# name: test_conditional_graph[sqlite].4
'''
graph TD;
PromptTemplate --> FakeStreamingListLLM;
FakeStreamingListLLM --> agent_parser;
Parallel_agent_outcome_Input --> PromptTemplate;
agent_parser --> Parallel_agent_outcome_Output;
Parallel_agent_outcome_Input --> Passthrough;
Passthrough --> Parallel_agent_outcome_Output;
__start__ --> Parallel_agent_outcome_Input;
tools --> Parallel_agent_outcome_Input;
Parallel_agent_outcome_Output -. &nbsp;continue&nbsp; .-> tools;
Parallel_agent_outcome_Output -. &nbsp;exit&nbsp; .-> __end__;
__start__ --> agent;
tools --> agent;
agent -. &nbsp;continue&nbsp; .-> tools;
agent -. &nbsp;exit&nbsp; .-> __end__;
'''
# ---
@@ -5582,6 +5108,81 @@
'''
# ---
# name: test_send_react_interrupt_control[memory]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres_pipe]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres_pool]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[sqlite]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_simple_multi_edge
'''
graph TD;
@@ -1302,6 +1302,81 @@
+---------+
'''
# ---
# name: test_send_react_interrupt_control[memory]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres_aio]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres_aio_pipe]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[postgres_aio_pool]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_send_react_interrupt_control[sqlite_aio]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
agent(agent)
foo([foo]):::last
__start__ --> agent;
agent -.-> foo;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
'''
# ---
# name: test_weather_subgraph[duckdb_aio]
'''
%%{init: {'flowchart': {'curve': 'linear'}}}%%
+6 -3
View File
@@ -21,7 +21,8 @@ from langgraph.store.base import BaseStore
from langgraph.store.duckdb import AsyncDuckDBStore, DuckDBStore
from langgraph.store.memory import InMemoryStore
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
from tests.memory_assert import MemorySaverAssertImmutable
pytest.register_assert_rewrite("tests.memory_assert")
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
# TODO: fix this once core is released
@@ -49,6 +50,8 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
@pytest.fixture(scope="function")
def checkpointer_memory():
from tests.memory_assert import MemorySaverAssertImmutable
yield MemorySaverAssertImmutable()
@@ -225,6 +228,8 @@ async def awith_checkpointer(
if checkpointer_name is None:
yield None
elif checkpointer_name == "memory":
from tests.memory_assert import MemorySaverAssertImmutable
yield MemorySaverAssertImmutable()
elif checkpointer_name == "sqlite_aio":
async with _checkpointer_sqlite_aio() as checkpointer:
@@ -322,7 +327,6 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
ALL_CHECKPOINTERS_SYNC = [
"memory",
"sqlite",
"duckdb",
"postgres",
"postgres_pipe",
"postgres_pool",
@@ -330,7 +334,6 @@ ALL_CHECKPOINTERS_SYNC = [
ALL_CHECKPOINTERS_ASYNC = [
"memory",
"sqlite_aio",
"duckdb_aio",
"postgres_aio",
"postgres_aio_pipe",
"postgres_aio_pool",
+9 -2
View File
@@ -1,5 +1,8 @@
import asyncio
import os
import tempfile
from collections import defaultdict
from functools import partial
from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
@@ -12,7 +15,7 @@ from langgraph.checkpoint.base import (
SerializerProtocol,
copy_checkpoint,
)
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.memory import MemorySaver, PersistentDict
class NoopSerializer(SerializerProtocol):
@@ -32,9 +35,13 @@ class MemorySaverAssertImmutable(MemorySaver):
serde: Optional[SerializerProtocol] = None,
put_sleep: Optional[float] = None,
) -> None:
super().__init__(serde=serde)
_, filename = tempfile.mkstemp()
super().__init__(
serde=serde, factory=partial(PersistentDict, filename=filename)
)
self.storage_for_copies = defaultdict(lambda: defaultdict(dict))
self.put_sleep = put_sleep
self.stack.callback(os.remove, filename)
def put(
self,
+9 -1
View File
@@ -11,13 +11,21 @@ def test_prepare_next_tasks() -> None:
with ChannelsManager({}, checkpoint, config) as (channels, managed):
assert (
prepare_next_tasks(
checkpoint, processes, channels, managed, config, 0, for_execution=False
checkpoint,
{},
processes,
channels,
managed,
config,
0,
for_execution=False,
)
== {}
)
assert (
prepare_next_tasks(
checkpoint,
{},
processes,
channels,
managed,
+2
View File
@@ -158,6 +158,7 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) ->
"source": "loop",
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
"step": 1,
"thread_id": "123",
}
assert saved.pending_writes == []
@@ -189,6 +190,7 @@ async def test_no_modifier_async(checkpointer_name: str) -> None:
"source": "loop",
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
"step": 1,
"thread_id": "123",
}
assert saved.pending_writes == []
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -73,7 +73,9 @@ def test_get_graph():
"__start__": DrawableNode(
id="__start__", name="__start__", data="__start__", metadata=None
),
"__end__": DrawableNode(id="__end__", name="__end__", data="__end__", metadata=None),
"__end__": DrawableNode(
id="__end__", name="__end__", data="__end__", metadata=None
),
"agent": DrawableNode(
id="agent",
name="agent_1",
@@ -120,7 +122,9 @@ async def test_aget_graph():
"__start__": DrawableNode(
id="__start__", name="__start__", data="__start__", metadata=None
),
"__end__": DrawableNode(id="__end__", name="__end__", data="__end__", metadata=None),
"__end__": DrawableNode(
id="__end__", name="__end__", data="__end__", metadata=None
),
"agent": DrawableNode(
id="agent",
name="agent_1",
+34 -2
View File
@@ -5,11 +5,11 @@ from typing import Annotated as Annotated2
from typing import Any, Optional
import pytest
from langchain_core.runnables import RunnableConfig
from langchain_core.runnables import RunnableConfig, RunnableLambda
from pydantic.v1 import BaseModel
from typing_extensions import Annotated, NotRequired, Required, TypedDict
from langgraph.graph.state import StateGraph, _warn_invalid_state_schema
from langgraph.graph.state import StateGraph, _get_node_name, _warn_invalid_state_schema
from langgraph.managed.shared_value import SharedValue
@@ -286,3 +286,35 @@ def test_raises_invalid_managed():
match="Invalid managed channels detected in BadOutputState: some_output_channel. Managed channels are not permitted in Input/Output schema.",
):
StateGraph(_state, input=_inp, output=_outp)
def test__get_node_name() -> None:
# default runnable name
assert _get_node_name(RunnableLambda(func=lambda x: x)) == "RunnableLambda"
# custom runnable name
assert (
_get_node_name(RunnableLambda(name="my_runnable", func=lambda x: x))
== "my_runnable"
)
# lambda
assert _get_node_name(lambda x: x) == "<lambda>"
# regular function
def func(state):
return
assert _get_node_name(func) == "func"
class MyClass:
def __call__(self, state):
return
def class_method(self, state):
return
# callable class
assert _get_node_name(MyClass()) == "MyClass"
# class method
assert _get_node_name(MyClass().class_method) == "class_method"
+1 -1
View File
@@ -19,7 +19,7 @@ test:
exit $$EXIT_CODE
test_watch:
make start-services && poetry run ptw . -- $(TEST_PATH); \
make start-services && poetry run ptw . -- -x $(TEST_PATH); \
EXIT_CODE=$$?; \
make stop-services; \
exit $$EXIT_CODE
@@ -8,6 +8,7 @@ from contextlib import (
)
from functools import partial
from typing import Any, Optional, Sequence
from uuid import UUID
import orjson
from langchain_core.runnables import RunnableConfig
@@ -37,7 +38,7 @@ from langgraph.scheduler.kafka.types import (
Sendable,
Topics,
)
from langgraph.types import LoopProtocol, RetryPolicy
from langgraph.types import LoopProtocol, PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
@@ -197,6 +198,7 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
msg["task"]["path"],
msg["task"]["id"],
checkpoint=saved.checkpoint,
pending_writes=saved.pending_writes or [],
processes=graph.nodes,
channels=channels,
managed=managed,
@@ -210,13 +212,14 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
runner = PregelRunner(
submit=submit,
put_writes=partial(self._put_writes, submit, msg["config"]),
schedule_task=self._schedule_task,
)
async for _ in runner.atick([task], reraise=False):
pass
else:
# task was not found
await self.graph.checkpointer.aput_writes(
msg["config"], [(ERROR, TaskNotFound())]
msg["config"], [(ERROR, TaskNotFound())], str(UUID(int=0))
)
# notify orchestrator
fut = await self.producer.send(
@@ -238,6 +241,14 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
)
await fut
def _schedule_task(
self,
task: PregelExecutableTask,
idx: int,
) -> None:
# will be scheduled by orchestrator when executor finishes
pass
def _put_writes(
self,
submit: Submit,
@@ -399,6 +410,7 @@ class KafkaExecutor(AbstractContextManager):
msg["task"]["path"],
msg["task"]["id"],
checkpoint=saved.checkpoint,
pending_writes=saved.pending_writes or [],
processes=graph.nodes,
channels=channels,
managed=managed,
@@ -411,13 +423,14 @@ class KafkaExecutor(AbstractContextManager):
runner = PregelRunner(
submit=submit,
put_writes=partial(self._put_writes, submit, msg["config"]),
schedule_task=self._schedule_task,
)
for _ in runner.tick([task], reraise=False):
pass
else:
# task was not found
self.graph.checkpointer.put_writes(
msg["config"], [(ERROR, TaskNotFound())]
msg["config"], [(ERROR, TaskNotFound())], str(UUID(int=0))
)
# notify orchestrator
fut = self.producer.send(
@@ -439,6 +452,14 @@ class KafkaExecutor(AbstractContextManager):
)
fut.result()
def _schedule_task(
self,
task: PregelExecutableTask,
idx: int,
) -> None:
# will be scheduled by orchestrator when executor finishes
pass
def _put_writes(
self,
submit: Submit,
@@ -161,18 +161,18 @@ class AsyncKafkaOrchestrator(AbstractAsyncContextManager):
specs=graph.channels,
output_keys=graph.output_channels,
stream_keys=graph.stream_channels,
interrupt_after=graph.interrupt_after_nodes,
interrupt_before=graph.interrupt_before_nodes,
check_subgraphs=False,
) as loop:
if loop.tick(
input_keys=graph.input_channels,
interrupt_after=graph.interrupt_after_nodes,
interrupt_before=graph.interrupt_before_nodes,
):
if loop.tick(input_keys=graph.input_channels):
# wait for checkpoint to be saved
if hasattr(loop, "_put_checkpoint_fut"):
await loop._put_checkpoint_fut
# schedule any new tasks
if new_tasks := [t for t in loop.tasks.values() if not t.scheduled]:
if new_tasks := [
t for t in loop.tasks.values() if not t.scheduled and not t.writes
]:
# send messages to executor
futures = await asyncio.gather(
*(
@@ -351,18 +351,18 @@ class KafkaOrchestrator(AbstractContextManager):
specs=graph.channels,
output_keys=graph.output_channels,
stream_keys=graph.stream_channels,
interrupt_after=graph.interrupt_after_nodes,
interrupt_before=graph.interrupt_before_nodes,
check_subgraphs=False,
) as loop:
if loop.tick(
input_keys=graph.input_channels,
interrupt_after=graph.interrupt_after_nodes,
interrupt_before=graph.interrupt_before_nodes,
):
if loop.tick(input_keys=graph.input_channels):
# wait for checkpoint to be saved
if hasattr(loop, "_put_checkpoint_fut"):
loop._put_checkpoint_fut.result()
# schedule any new tasks
if new_tasks := [t for t in loop.tasks.values() if not t.scheduled]:
if new_tasks := [
t for t in loop.tasks.values() if not t.scheduled and not t.writes
]:
# send messages to executor
futures = [
self.producer.send(
@@ -24,8 +24,8 @@ class MessageToOrchestrator(TypedDict):
class ExecutorTask(TypedDict):
id: str
path: tuple[str, ...]
id: Optional[str]
path: tuple[Union[str, int], ...]
class MessageToExecutor(TypedDict):
+208
View File
@@ -0,0 +1,208 @@
import operator
from typing import (
Annotated,
Literal,
Union,
)
import pytest
from aiokafka import AIOKafkaProducer
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import FF_SEND_V2, START
from langgraph.errors import NodeInterrupt
from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
from langgraph.types import Send
from tests.any import AnyDict
from tests.drain import drain_topics_async
pytestmark = pytest.mark.anyio
def mk_push_graph(
checkpointer: BaseCheckpointSaver,
) -> CompiledStateGraph:
# copied from test_send_dedupe_on_resume
class InterruptOnce:
ticks: int = 0
def __call__(self, state):
self.ticks += 1
if self.ticks == 1:
raise NodeInterrupt("Bahh")
return ["|".join(("flaky", str(state)))]
class Node:
def __init__(self, name: str):
self.name = name
self.ticks = 0
self.__name__ = name
def __call__(self, state):
self.ticks += 1
update = (
[self.name]
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
if isinstance(state, GraphCommand):
return state.copy(update=update)
else:
return update
def send_for_fun(state):
return [
Send("2", GraphCommand(send=Send("2", 3))),
Send("2", GraphCommand(send=Send("flaky", 4))),
"3.1",
]
def route_to_three(state) -> Literal["3"]:
return "3"
builder = StateGraph(Annotated[list, operator.add])
builder.add_node(Node("1"))
builder.add_node(Node("2"))
builder.add_node(Node("3"))
builder.add_node(Node("3.1"))
builder.add_node("flaky", InterruptOnce())
builder.add_edge(START, "1")
builder.add_conditional_edges("1", send_for_fun)
builder.add_conditional_edges("2", route_to_three)
return builder.compile(checkpointer=checkpointer)
async def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
if not FF_SEND_V2:
pytest.skip("Test requires FF_SEND_V2")
input = ["0"]
config = {"configurable": {"thread_id": "1"}}
graph = mk_push_graph(acheckpointer)
graph_compare = mk_push_graph(acheckpointer)
# start a new run
async with AIOKafkaProducer(value_serializer=serde.dumps) as producer:
await producer.send_and_wait(
topics.orchestrator,
MessageToOrchestrator(input=input, config=config),
)
# drain topics
orch_msgs, exec_msgs = await drain_topics_async(topics, graph)
# check state
state = await graph.aget_state(config)
assert all(not t.error for t in state.tasks)
assert state.next == ("flaky",)
assert (
state.values
== await graph_compare.ainvoke(input, {"configurable": {"thread_id": "2"}})
== [
"0",
"1",
"2|Control(send=Send(node='2', arg=3))",
"2|Control(send=Send(node='flaky', arg=4))",
"2|3",
]
)
# check history
history = [c async for c in graph.aget_state_history(config)]
assert len(history) == 2
# check messages
assert orch_msgs == [MessageToOrchestrator(input=input, config=config)] + [
{
"config": {
"callbacks": None,
"configurable": {
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_ns": "",
"thread_id": "1",
},
"metadata": AnyDict(),
"recursion_limit": 25,
"tags": [],
},
"input": None,
"finally_send": None,
}
for c in reversed(history)
for _ in c.tasks
]
assert exec_msgs == [
{
"config": {
"callbacks": None,
"configurable": {
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_ns": "",
"thread_id": "1",
},
"metadata": AnyDict(),
"recursion_limit": 25,
"tags": [],
},
"task": {
"id": t.id,
"path": _convert_path(t.path),
},
"finally_send": None,
}
for c in reversed(history)
for t in c.tasks
]
# resume the thread
async with AIOKafkaProducer(value_serializer=serde.dumps) as producer:
await producer.send_and_wait(
topics.orchestrator,
MessageToOrchestrator(input=None, config=config),
)
orch_msgs, exec_msgs = await drain_topics_async(topics, graph)
# check final state
state = await graph.aget_state(config)
assert state.next == ()
assert (
state.values
== await graph_compare.ainvoke(None, {"configurable": {"thread_id": "2"}})
== [
"0",
"1",
"2|Control(send=Send(node='2', arg=3))",
"2|Control(send=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
"3.1",
]
)
# check history
history = [c async for c in graph.aget_state_history(config)]
assert len(history) == 4
# check executions
# node "2" doesn't get called again, as we recover writes saved before
assert graph.builder.nodes["2"].runnable.func.ticks == 3
# node "flaky" gets called again, as it was interrupted
assert graph.builder.nodes["flaky"].runnable.func.ticks == 2
def _convert_path(
path: tuple[Union[str, int, tuple], ...],
) -> list[Union[str, int, list]]:
return list(_convert_path(p) if isinstance(p, tuple) else p for p in path)
@@ -0,0 +1,210 @@
import operator
from typing import (
Annotated,
Literal,
Union,
)
import pytest
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import FF_SEND_V2, START
from langgraph.errors import NodeInterrupt
from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.default_sync import DefaultProducer
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
from langgraph.types import Send
from tests.any import AnyDict
from tests.drain import drain_topics
pytestmark = pytest.mark.anyio
def mk_push_graph(
checkpointer: BaseCheckpointSaver,
) -> CompiledStateGraph:
# copied from test_send_dedupe_on_resume
class InterruptOnce:
ticks: int = 0
def __call__(self, state):
self.ticks += 1
if self.ticks == 1:
raise NodeInterrupt("Bahh")
return ["|".join(("flaky", str(state)))]
class Node:
def __init__(self, name: str):
self.name = name
self.ticks = 0
self.__name__ = name
def __call__(self, state):
self.ticks += 1
update = (
[self.name]
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
if isinstance(state, GraphCommand):
return state.copy(update=update)
else:
return update
def send_for_fun(state):
return [
Send("2", GraphCommand(send=Send("2", 3))),
Send("2", GraphCommand(send=Send("flaky", 4))),
"3.1",
]
def route_to_three(state) -> Literal["3"]:
return "3"
builder = StateGraph(Annotated[list, operator.add])
builder.add_node(Node("1"))
builder.add_node(Node("2"))
builder.add_node(Node("3"))
builder.add_node(Node("3.1"))
builder.add_node("flaky", InterruptOnce())
builder.add_edge(START, "1")
builder.add_conditional_edges("1", send_for_fun)
builder.add_conditional_edges("2", route_to_three)
return builder.compile(checkpointer=checkpointer)
def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
if not FF_SEND_V2:
pytest.skip("Test requires FF_SEND_V2")
input = ["0"]
config = {"configurable": {"thread_id": "1"}}
graph = mk_push_graph(acheckpointer)
graph_compare = mk_push_graph(acheckpointer)
# start a new run
with DefaultProducer() as producer:
producer.send(
topics.orchestrator,
value=serde.dumps(MessageToOrchestrator(input=input, config=config)),
)
producer.flush()
# drain topics
orch_msgs, exec_msgs = drain_topics(topics, graph)
# check state
state = graph.get_state(config)
assert all(not t.error for t in state.tasks)
assert state.next == ("flaky",)
assert (
state.values
== graph_compare.invoke(input, {"configurable": {"thread_id": "2"}})
== [
"0",
"1",
"2|Control(send=Send(node='2', arg=3))",
"2|Control(send=Send(node='flaky', arg=4))",
"2|3",
]
)
# check history
history = [c for c in graph.get_state_history(config)]
assert len(history) == 2
# check messages
assert orch_msgs == [MessageToOrchestrator(input=input, config=config)] + [
{
"config": {
"callbacks": None,
"configurable": {
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_ns": "",
"thread_id": "1",
},
"metadata": AnyDict(),
"recursion_limit": 25,
"tags": [],
},
"input": None,
"finally_send": None,
}
for c in reversed(history)
for _ in c.tasks
]
assert exec_msgs == [
{
"config": {
"callbacks": None,
"configurable": {
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_ns": "",
"thread_id": "1",
},
"metadata": AnyDict(),
"recursion_limit": 25,
"tags": [],
},
"task": {
"id": t.id,
"path": _convert_path(t.path),
},
"finally_send": None,
}
for c in reversed(history)
for t in c.tasks
]
# resume the thread
with DefaultProducer() as producer:
producer.send(
topics.orchestrator,
value=serde.dumps(MessageToOrchestrator(input=None, config=config)),
)
producer.flush()
orch_msgs, exec_msgs = drain_topics(topics, graph)
# check final state
state = graph.get_state(config)
assert state.next == ()
assert (
state.values
== graph_compare.invoke(None, {"configurable": {"thread_id": "2"}})
== [
"0",
"1",
"2|Control(send=Send(node='2', arg=3))",
"2|Control(send=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
"3.1",
]
)
# check history
history = [c for c in graph.get_state_history(config)]
assert len(history) == 4
# check executions
# node "2" doesn't get called again, as we recover writes saved before
assert graph.builder.nodes["2"].runnable.func.ticks == 3
# node "flaky" gets called again, as it was interrupted
assert graph.builder.nodes["flaky"].runnable.func.ticks == 2
def _convert_path(
path: tuple[Union[str, int, tuple], ...],
) -> list[Union[str, int, list]]:
return list(_convert_path(p) if isinstance(p, tuple) else p for p in path)
+12 -6
View File
@@ -194,8 +194,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": None,
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -258,8 +259,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -352,8 +354,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -456,8 +459,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": None,
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -515,8 +519,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -630,8 +635,9 @@ async def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -193,8 +193,9 @@ def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": None,
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -255,10 +256,11 @@ def test_subgraph_w_interrupt(
"__pregel_read": None,
"__pregel_send": None,
"__pregel_ensure_latest": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -350,9 +352,10 @@ def test_subgraph_w_interrupt(
"__pregel_send": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_resuming": False,
"__pregel_task_id": history[0].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -453,9 +456,10 @@ def test_subgraph_w_interrupt(
"__pregel_send": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_resuming": True,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": None,
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -512,9 +516,10 @@ def test_subgraph_w_interrupt(
"__pregel_send": None,
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_resuming": True,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -628,8 +633,9 @@ def test_subgraph_w_interrupt(
"__pregel_ensure_latest": True,
"__pregel_dedupe_tasks": True,
"__pregel_resuming": True,
'__pregel_store': None,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
"__pregel_resume_value": None,
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
+11 -13
View File
@@ -1,13 +1,11 @@
/docs
/*.tgz
/*.tar
## GENERATED create-entrypoints.js
/client.cjs
/client.js
/client.d.ts
/client.d.cts
/index.cjs
/index.js
/index.d.ts
/index.d.cts
## END GENERATED create-entrypoints.js
index.cjs
index.js
index.d.ts
index.d.cts
client.cjs
client.js
client.d.ts
client.d.cts
node_modules
dist
.yarn
+19
View File
@@ -0,0 +1,19 @@
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
/**
* @param {string} relativePath
* @returns {string}
*/
function abs(relativePath) {
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
}
export const config = {
internals: [],
entrypoints: { index: "index", client: "client" },
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
abs,
};
+9 -10
View File
@@ -1,14 +1,12 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.20",
"version": "0.0.25",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
"scripts": {
"clean": "rm -rf dist/ && node scripts/create-entrypoints.js clean",
"build": "yarn clean && yarn build:esm && yarn build:cjs && node scripts/create-entrypoints.js",
"build:esm": "rm -f src/package.json && tsc --outDir dist/ && rm -rf dist/tests dist/**/tests",
"build:cjs": "echo '{}' > src/package.json && tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs src/package.json",
"clean": "rm -rf dist/ dist-cjs/",
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
"prepublish": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit"
@@ -22,6 +20,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@langchain/scripts": "^0.1.4",
"@tsconfig/recommended": "^1.0.2",
"@types/node": "^20.12.12",
"@types/uuid": "^9.0.1",
@@ -54,13 +53,13 @@
},
"files": [
"dist/",
"client.cjs",
"client.js",
"client.d.ts",
"client.d.cts",
"index.cjs",
"index.js",
"index.d.ts",
"index.d.cts"
"index.d.cts",
"client.cjs",
"client.js",
"client.d.ts",
"client.d.cts"
]
}
-129
View File
@@ -1,129 +0,0 @@
import * as fs from "fs";
import * as path from "path";
// This lists all the entrypoints for the library. Each key corresponds to an
// importable path, eg. `import { Foo } from "langgraph-sdk/client"`.
// The value is the path to the file in `src/` that exports the entrypoint.
// This is used to generate the `exports` field in package.json.
// Order is not important.
const entrypoints = { client: "client" };
const updateJsonFile = (relativePath, updateFunction) => {
const contents = fs.readFileSync(relativePath).toString();
const res = updateFunction(JSON.parse(contents));
fs.writeFileSync(relativePath, JSON.stringify(res, null, 2) + "\n");
};
const generateFiles = () => {
const files = [...Object.entries(entrypoints), ["index", "index"]].flatMap(
([key, value]) => {
const nrOfDots = key.split("/").length - 1;
const relativePath = "../".repeat(nrOfDots) || "./";
const compiledPath = `${relativePath}dist/${value}`;
return [
[`${key}.cjs`, `module.exports = require('${compiledPath}.cjs');`],
[`${key}.js`, `export * from '${compiledPath}.js'`],
[`${key}.d.ts`, `export * from '${compiledPath}.js'`],
[`${key}.d.cts`, `export * from '${compiledPath}.cjs'`],
];
},
);
return Object.fromEntries(files);
};
const updateConfig = () => {
// Update tsconfig.json `typedocOptions.entryPoints` field
updateJsonFile("./tsconfig.json", (json) => ({
...json,
typedocOptions: {
...json.typedocOptions,
entryPoints: [...Object.keys(entrypoints)].map((key) => {
const basePath = `src/${entrypoints[key]}`;
if (fs.existsSync(`${basePath}.mts`)) {
return `${basePath}.mts`;
} else if (fs.existsSync(`${basePath}.ts`)) {
return `${basePath}.ts`;
} else {
console.warn(
`Warning: Neither ${basePath}.mts nor ${basePath}.ts found for entrypoint ${key}`,
);
return `${basePath}.ts`; // Default to .ts if neither exists
}
}),
},
}));
const generatedFiles = generateFiles();
const filenames = Object.keys(generatedFiles);
// Update package.json `exports` and `files` fields
updateJsonFile("./package.json", (json) => ({
...json,
exports: Object.assign(
Object.fromEntries(
["index", ...Object.keys(entrypoints)].map((key) => {
let entryPoint = {
types: {
import: `./${key}.d.ts`,
require: `./${key}.d.cts`,
default: `./${key}.d.ts`,
},
import: `./${key}.js`,
require: `./${key}.cjs`,
};
return [key === "index" ? "." : `./${key}`, entryPoint];
}),
),
{
"./package.json": "./package.json",
},
),
files: ["dist/", ...filenames],
}));
// Write generated files
Object.entries(generatedFiles).forEach(([filename, content]) => {
fs.mkdirSync(path.dirname(filename), {
recursive: true,
});
fs.writeFileSync(filename, content);
});
const gitignore = fs.readFileSync("./.gitignore").toString();
const lines = gitignore.split("\n");
const startMarker = "## GENERATED create-entrypoints.js";
const endMarker = "## END GENERATED create-entrypoints.js";
const startIdx = lines.findIndex((line) => line.includes(startMarker));
const endIdx = lines.findIndex((line) => line.includes(endMarker));
const newLines = lines.slice(0, startIdx + 1);
if (startIdx === -1) {
newLines.push(startMarker);
}
newLines.push(...filenames.map((fname) => `/${fname}`));
if (endIdx === -1) {
newLines.push(endMarker);
}
newLines.push(...lines.slice(endIdx));
fs.writeFileSync("./.gitignore", newLines.join("\n"));
};
const cleanGenerated = () => {
const filenames = Object.keys(generateFiles());
filenames.forEach((fname) => {
try {
fs.unlinkSync(fname);
} catch {
// ignore error
}
});
};
const command = process.argv[2];
if (command === "clean") {
cleanGenerated();
} else {
updateConfig();
}
-38
View File
@@ -1,38 +0,0 @@
import { resolve, dirname, parse, format } from "node:path";
import { readdir, readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
function abs(relativePath) {
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
}
async function moveAndRename(source, dest) {
for (const file of await readdir(abs(source), { withFileTypes: true })) {
if (file.isDirectory()) {
await moveAndRename(`${source}/${file.name}`, `${dest}/${file.name}`);
} else if (file.isFile()) {
const parsed = parse(file.name);
// Ignore anything that's not a .js file
if (parsed.ext !== ".js") {
continue;
}
// Rewrite any require statements to use .cjs
const content = await readFile(abs(`${source}/${file.name}`), "utf8");
const rewritten = content.replace(/require\("(\..+?).js"\)/g, (_, p1) => {
return `require("${p1}.cjs")`;
});
// Rename the file to .cjs
const renamed = format({ name: parsed.name, ext: ".cjs" });
await writeFile(abs(`${dest}/${renamed}`), rewritten, "utf8");
}
}
}
moveAndRename("../dist-cjs", "../dist").catch((err) => {
console.error(err);
process.exit(1);
});
+10
View File
@@ -16,6 +16,7 @@ import {
SearchItemsResponse,
ListNamespaceResponse,
Item,
ThreadStatus,
} from "./schema.js";
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
import {
@@ -516,6 +517,11 @@ export class ThreadsClient extends BaseClient {
* Offset to start from.
*/
offset?: number;
/**
* Thread status to filter on.
* Must be one of 'idle', 'busy', 'interrupted' or 'error'.
*/
status?: ThreadStatus;
}): Promise<Thread[]> {
return this.fetch<Thread[]>("/threads/search", {
method: "POST",
@@ -523,6 +529,7 @@ export class ThreadsClient extends BaseClient {
metadata: query?.metadata ?? undefined,
limit: query?.limit ?? 10,
offset: query?.offset ?? 0,
status: query?.status,
},
});
}
@@ -686,6 +693,7 @@ export class RunsClient extends BaseClient {
}> {
const json: Record<string, any> = {
input: payload?.input,
command: payload?.command,
config: payload?.config,
metadata: payload?.metadata,
stream_mode: payload?.streamMode,
@@ -773,6 +781,7 @@ export class RunsClient extends BaseClient {
): Promise<Run> {
const json: Record<string, any> = {
input: payload?.input,
command: payload?.command,
config: payload?.config,
metadata: payload?.metadata,
assistant_id: assistantId,
@@ -842,6 +851,7 @@ export class RunsClient extends BaseClient {
): Promise<ThreadState["values"]> {
const json: Record<string, any> = {
input: payload?.input,
command: payload?.command,
config: payload?.config,
metadata: payload?.metadata,
assistant_id: assistantId,
+2
View File
@@ -10,7 +10,9 @@ export type {
Metadata,
Run,
Thread,
ThreadTask,
ThreadState,
ThreadStatus,
Cron,
Checkpoint,
} from "./schema.js";
+35 -4
View File
@@ -10,7 +10,7 @@ type RunStatus =
| "timeout"
| "interrupted";
type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
export type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
@@ -79,7 +79,17 @@ export interface GraphSchema {
export type Subgraphs = Record<string, GraphSchema>;
export type Metadata = Optional<Record<string, unknown>>;
export type Metadata = Optional<{
source?: "input" | "loop" | "update" | (string & {});
step?: number;
writes?: Record<string, unknown> | null;
parents?: Record<string, string>;
[key: string]: unknown;
}>;
export interface AssistantBase {
/** The ID of the assistant. */
@@ -110,7 +120,22 @@ export interface Assistant extends AssistantBase {
/** The name of the assistant */
name: string;
}
export type AssistantGraph = Record<string, Array<Record<string, unknown>>>;
export interface AssistantGraph {
nodes: Array<{
id: string | number;
name?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: Record<string, any> | string;
metadata?: unknown;
}>;
edges: Array<{
source: string;
target: string;
data?: string;
conditional?: boolean;
}>;
}
export interface Thread<ValuesType = DefaultValues> {
/** The ID of the thread. */
@@ -183,8 +208,14 @@ export interface ThreadState<ValuesType = DefaultValues> {
export interface ThreadTask {
id: string;
name: string;
result?: unknown;
error: Optional<string>;
interrupts: Array<Record<string, unknown>>;
interrupts: Array<{
value: unknown;
when: "during";
resumable: boolean;
ns?: string[];
}>;
checkpoint: Optional<Checkpoint>;
state: Optional<ThreadState>;
}
+28 -3
View File
@@ -1,6 +1,13 @@
import { Checkpoint, Config, Metadata } from "./schema.js";
export type StreamMode = "values" | "messages" | "updates" | "events" | "debug";
export type StreamMode =
| "values"
| "messages"
| "updates"
| "events"
| "debug"
| "custom"
| "messages-tuple";
export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
export type OnConflictBehavior = "raise" | "do_nothing";
export type OnCompletionBehavior = "complete" | "continue";
@@ -16,6 +23,19 @@ export type StreamEvent =
| "messages/complete"
| (string & {});
export interface Send {
node: string;
input: Record<string, unknown> | null;
}
export interface Command {
update?: Record<string, unknown>;
resume?: unknown;
send?: Send | Send[];
}
interface RunsInvokePayload {
/**
* Input to the run. Pass `null` to resume from the current state of the thread.
@@ -45,12 +65,12 @@ interface RunsInvokePayload {
/**
* Interrupt execution before entering these nodes.
*/
interruptBefore?: string[];
interruptBefore?: "*" | string[];
/**
* Interrupt execution after leaving these nodes.
*/
interruptAfter?: string[];
interruptAfter?: "*" | string[];
/**
* Strategy to handle concurrent runs on the same thread. Only relevant if
@@ -100,6 +120,11 @@ interface RunsInvokePayload {
* Behavior if the specified run doesn't exist. Defaults to "reject".
*/
ifNotExists?: "create" | "reject";
/**
* One or more commands to invoke the graph with.
*/
command?: Command;
}
export interface RunsStreamPayload extends RunsInvokePayload {
+2
View File
@@ -1,6 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"declaration": false
},
"exclude": ["node_modules", "dist", "**/tests"]
+608 -1
View File
@@ -25,6 +25,35 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@langchain/scripts@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@langchain/scripts/-/scripts-0.1.4.tgz#8c5d03d627686f20b9522213c12e2b329e73e381"
integrity sha512-O+mv2aqUIm3XWxYBrwFWMeTI3aHWeFR8OYjGFXKc1MGV/3LLao3PciyQvKUg1SL7FemHJ1ltDx74rKuEv8xxPA==
dependencies:
"@octokit/rest" "^21.0.2"
"@rollup/wasm-node" "^4.19.0"
axios "^1.6.7"
commander "^11.1.0"
glob "^10.3.10"
lodash "^4.17.21"
readline "^1.3.0"
rimraf "^5.0.1"
rollup "^4.5.2"
ts-morph "^21.0.1"
typescript "^5.4.5"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -46,6 +75,203 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@octokit/auth-token@^5.0.0":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.1.tgz#3bbfe905111332a17f72d80bd0b51a3e2fa2cf07"
integrity sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==
"@octokit/core@^6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.2.tgz#20442d0a97c411612da206411e356014d1d1bd17"
integrity sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==
dependencies:
"@octokit/auth-token" "^5.0.0"
"@octokit/graphql" "^8.0.0"
"@octokit/request" "^9.0.0"
"@octokit/request-error" "^6.0.1"
"@octokit/types" "^13.0.0"
before-after-hook "^3.0.2"
universal-user-agent "^7.0.0"
"@octokit/endpoint@^10.0.0":
version "10.1.1"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.1.tgz#1a9694e7aef6aa9d854dc78dd062945945869bcc"
integrity sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==
dependencies:
"@octokit/types" "^13.0.0"
universal-user-agent "^7.0.2"
"@octokit/graphql@^8.0.0":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.1.1.tgz#3cacab5f2e55d91c733e3bf481d3a3f8a5f639c4"
integrity sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==
dependencies:
"@octokit/request" "^9.0.0"
"@octokit/types" "^13.0.0"
universal-user-agent "^7.0.0"
"@octokit/openapi-types@^22.2.0":
version "22.2.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e"
integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==
"@octokit/plugin-paginate-rest@^11.0.0":
version "11.3.5"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.5.tgz#a1929b3ba3dc7b63bc73bb6d3c7a3faf2a9c7649"
integrity sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==
dependencies:
"@octokit/types" "^13.6.0"
"@octokit/plugin-request-log@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69"
integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==
"@octokit/plugin-rest-endpoint-methods@^13.0.0":
version "13.2.6"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz#b9d343dbe88a6cb70cc7fa16faa98f0a29ffe654"
integrity sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==
dependencies:
"@octokit/types" "^13.6.1"
"@octokit/request-error@^6.0.1":
version "6.1.5"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.5.tgz#907099e341c4e6179db623a0328d678024f54653"
integrity sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==
dependencies:
"@octokit/types" "^13.0.0"
"@octokit/request@^9.0.0":
version "9.1.3"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.1.3.tgz#42b693bc06238f43af3c037ebfd35621c6457838"
integrity sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==
dependencies:
"@octokit/endpoint" "^10.0.0"
"@octokit/request-error" "^6.0.1"
"@octokit/types" "^13.1.0"
universal-user-agent "^7.0.2"
"@octokit/rest@^21.0.2":
version "21.0.2"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.0.2.tgz#9b767dbc1098daea8310fd8b76bf7a97215d5972"
integrity sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==
dependencies:
"@octokit/core" "^6.1.2"
"@octokit/plugin-paginate-rest" "^11.0.0"
"@octokit/plugin-request-log" "^5.3.1"
"@octokit/plugin-rest-endpoint-methods" "^13.0.0"
"@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.6.0", "@octokit/types@^13.6.1":
version "13.6.1"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.1.tgz#432fc6c0aaae54318e5b2d3e15c22ac97fc9b15f"
integrity sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==
dependencies:
"@octokit/openapi-types" "^22.2.0"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@rollup/rollup-android-arm-eabi@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz#c460b54c50d42f27f8254c435a4f3b3e01910bc8"
integrity sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==
"@rollup/rollup-android-arm64@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz#96e01f3a04675d8d5973ab8d3fd6bc3be21fa5e1"
integrity sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==
"@rollup/rollup-darwin-arm64@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz#9b2ec23b17b47cbb2f771b81f86ede3ac6730bce"
integrity sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==
"@rollup/rollup-darwin-x64@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz#f30e4ee6929e048190cf10e0daa8e8ae035b6e46"
integrity sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==
"@rollup/rollup-freebsd-arm64@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz#c54b2373ec5bcf71f08c4519c7ae80a0b6c8e03b"
integrity sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==
"@rollup/rollup-freebsd-x64@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz#3bc53aa29d5a34c28ba8e00def76aa612368458e"
integrity sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==
"@rollup/rollup-linux-arm-gnueabihf@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz#c85aedd1710c9e267ee86b6d1ce355ecf7d9e8d9"
integrity sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==
"@rollup/rollup-linux-arm-musleabihf@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz#e77313408bf13995aecde281aec0cceb08747e42"
integrity sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==
"@rollup/rollup-linux-arm64-gnu@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz#633f632397b3662108cfaa1abca2a80b85f51102"
integrity sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==
"@rollup/rollup-linux-arm64-musl@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz#63edd72b29c4cced93e16113a68e1be9fef88907"
integrity sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==
"@rollup/rollup-linux-powerpc64le-gnu@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz#a9418a4173df80848c0d47df0426a0bf183c4e75"
integrity sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==
"@rollup/rollup-linux-riscv64-gnu@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz#bc9c195db036a27e5e3339b02f51526b4ce1e988"
integrity sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==
"@rollup/rollup-linux-s390x-gnu@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz#1651fdf8144ae89326c01da5d52c60be63e71a82"
integrity sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==
"@rollup/rollup-linux-x64-gnu@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz#e473de5e4acb95fcf930a35cbb7d3e8080e57a6f"
integrity sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==
"@rollup/rollup-linux-x64-musl@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz#0af12dd2578c29af4037f0c834b4321429dd5b01"
integrity sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==
"@rollup/rollup-win32-arm64-msvc@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz#e48e78cdd45313b977c1390f4bfde7ab79be8871"
integrity sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==
"@rollup/rollup-win32-ia32-msvc@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz#a3fc8536d243fe161c796acb93eba43c250f311c"
integrity sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==
"@rollup/rollup-win32-x64-msvc@4.24.4":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz#e2a9d1fd56524103a6cc8a54404d9d3ebc73c454"
integrity sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==
"@rollup/wasm-node@^4.19.0":
version "4.24.4"
resolved "https://registry.yarnpkg.com/@rollup/wasm-node/-/wasm-node-4.24.4.tgz#11d78f5cc85b04e81468f245c2dc0885d06d663d"
integrity sha512-WKJUdPcM8YAYujafY95+2EapqU3F/nwfBkXh9AfkBvWBwFhsvNJABA86Br6graRH2vRE4FBsiqjFvFWOtEO6wg==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
fsevents "~2.3.2"
"@shikijs/core@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.9.0.tgz#ff717fef5e0e9882f0848272699fd8f04d6f9a07"
@@ -71,11 +297,26 @@
traverse "^0.6.7"
unified "^9.2.2"
"@ts-morph/common@~0.22.0":
version "0.22.0"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.22.0.tgz#8951d451622a26472fbc3a227d6c3a90e687a683"
integrity sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==
dependencies:
fast-glob "^3.3.2"
minimatch "^9.0.3"
mkdirp "^3.0.1"
path-browserify "^1.0.1"
"@tsconfig/recommended@^1.0.2":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.6.tgz#217b78f9601215939d566a79d202a760ae185114"
integrity sha512-0IKu9GHYF1NGTJiYgfWwqnOQSlnE9V9R7YohHNNf0/fj/SyOZWzdd06JFr0fLpg1Mqw0kGbYg8w5xdkSqLKM9g==
"@types/estree@1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@@ -127,6 +368,16 @@ anchor-markdown-header@^0.6.0:
dependencies:
emoji-regex "~10.1.0"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -134,6 +385,18 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -178,6 +441,11 @@ arrify@^1.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
@@ -185,6 +453,15 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axios@^1.6.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -195,6 +472,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
before-after-hook@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d"
integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
@@ -263,6 +545,11 @@ character-reference-invalid@^1.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
code-block-writer@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -270,11 +557,35 @@ color-convert@^1.9.0:
dependencies:
color-name "1.1.3"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
concat-md@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/concat-md/-/concat-md-0.5.1.tgz#03c72343a5d81306aa5ae1040d6368ffbc444781"
@@ -287,6 +598,15 @@ concat-md@^0.5.1:
meow "^9.0.0"
transform-markdown-links "^2.0.0"
cross-spawn@^7.0.0:
version "7.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
data-view-buffer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
@@ -352,6 +672,11 @@ define-properties@^1.2.0, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -401,6 +726,21 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
emoji-regex@~10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66"
@@ -542,7 +882,7 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-glob@^3.2.9:
fast-glob@^3.2.9, fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -582,6 +922,11 @@ find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -589,6 +934,23 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
foreground-child@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==
dependencies:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
form-data@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
format@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
@@ -601,6 +963,11 @@ front-matter@^4.0.2:
dependencies:
js-yaml "^3.13.1"
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
@@ -648,6 +1015,18 @@ glob-parent@^5.1.2:
dependencies:
is-glob "^4.0.1"
glob@^10.3.10, glob@^10.3.7:
version "10.4.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
minimatch "^9.0.4"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
globalthis@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
@@ -844,6 +1223,11 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@@ -931,6 +1315,20 @@ isarray@^2.0.5:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -978,11 +1376,21 @@ lodash.startcase@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8"
integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
longest-streak@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -1221,11 +1629,30 @@ micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimatch@^9.0.3:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
@@ -1247,6 +1674,16 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
mkdirp@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1339,6 +1776,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
parse-entities@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
@@ -1361,16 +1803,34 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
dependencies:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -1396,6 +1856,11 @@ prettier@^3.2.5:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
@@ -1430,6 +1895,11 @@ read-pkg@^5.2.0:
parse-json "^5.0.0"
type-fest "^0.6.0"
readline@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c"
integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -1503,6 +1973,40 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@^5.0.1:
version "5.0.10"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c"
integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==
dependencies:
glob "^10.3.7"
rollup@^4.5.2:
version "4.24.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.4.tgz#fdc76918de02213c95447c9ffff5e35dddb1d058"
integrity sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.24.4"
"@rollup/rollup-android-arm64" "4.24.4"
"@rollup/rollup-darwin-arm64" "4.24.4"
"@rollup/rollup-darwin-x64" "4.24.4"
"@rollup/rollup-freebsd-arm64" "4.24.4"
"@rollup/rollup-freebsd-x64" "4.24.4"
"@rollup/rollup-linux-arm-gnueabihf" "4.24.4"
"@rollup/rollup-linux-arm-musleabihf" "4.24.4"
"@rollup/rollup-linux-arm64-gnu" "4.24.4"
"@rollup/rollup-linux-arm64-musl" "4.24.4"
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.4"
"@rollup/rollup-linux-riscv64-gnu" "4.24.4"
"@rollup/rollup-linux-s390x-gnu" "4.24.4"
"@rollup/rollup-linux-x64-gnu" "4.24.4"
"@rollup/rollup-linux-x64-musl" "4.24.4"
"@rollup/rollup-win32-arm64-msvc" "4.24.4"
"@rollup/rollup-win32-ia32-msvc" "4.24.4"
"@rollup/rollup-win32-x64-msvc" "4.24.4"
fsevents "~2.3.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -1561,6 +2065,18 @@ set-function-name@^2.0.1:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shiki@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.9.0.tgz#e4d3a044d9c746aefbea47615e83323fdc3dc361"
@@ -1578,6 +2094,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
signal-exit@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -1614,6 +2135,33 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string.prototype.trim@^1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4"
@@ -1642,6 +2190,27 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
@@ -1692,6 +2261,14 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
ts-morph@^21.0.1:
version "21.0.1"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-21.0.1.tgz#712302a0f6e9dbf1aa8d9cf33a4386c4b18c2006"
integrity sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==
dependencies:
"@ts-morph/common" "~0.22.0"
code-block-writer "^12.0.0"
type-fest@^0.18.0:
version "0.18.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
@@ -1841,6 +2418,11 @@ unist-util-visit-parents@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e"
integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==
update-section@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/update-section/-/update-section-0.3.3.tgz#458f17820d37820dc60e20b86d94391b00123158"
@@ -1899,6 +2481,31 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15:
gopd "^1.0.1"
has-tostringtag "^1.0.2"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+16
View File
@@ -36,6 +36,7 @@ from langgraph_sdk.schema import (
AssistantVersion,
CancelAction,
Checkpoint,
Command,
Config,
Cron,
DisconnectMode,
@@ -1174,6 +1175,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1197,6 +1199,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1217,6 +1220,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1241,6 +1245,7 @@ class RunsClient:
assistant_id: The assistant ID or graph name to stream from.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
command: A command to execute. Cannot be combined with input.
stream_mode: The stream mode(s) to use.
stream_subgraphs: Whether to stream output from subgraphs.
metadata: Metadata to assign to the run.
@@ -1291,6 +1296,7 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": command,
"config": config,
"metadata": metadata,
"stream_mode": stream_mode,
@@ -1324,6 +1330,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1343,6 +1350,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1363,6 +1371,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -1385,6 +1394,7 @@ class RunsClient:
assistant_id: The assistant ID or graph name to stream from.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
command: A command to execute. Cannot be combined with input.
stream_mode: The stream mode(s) to use.
stream_subgraphs: Whether to stream output from subgraphs.
metadata: Metadata to assign to the run.
@@ -1471,6 +1481,7 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": command,
"stream_mode": stream_mode,
"stream_subgraphs": stream_subgraphs,
"config": config,
@@ -1508,6 +1519,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
checkpoint: Optional[Checkpoint] = None,
@@ -1529,6 +1541,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
@@ -1547,6 +1560,7 @@ class RunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
checkpoint: Optional[Checkpoint] = None,
@@ -1569,6 +1583,7 @@ class RunsClient:
assistant_id: The assistant ID or graph name to run.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
command: A command to execute. Cannot be combined with input.
metadata: Metadata to assign to the run.
config: The configuration for the assistant.
checkpoint: The checkpoint to resume from.
@@ -1635,6 +1650,7 @@ class RunsClient:
""" # noqa: E501
payload = {
"input": input,
"command": command,
"config": config,
"metadata": metadata,
"assistant_id": assistant_id,
+11
View File
@@ -339,3 +339,14 @@ class StreamPart(NamedTuple):
"""The type of event for this stream part."""
data: dict
"""The data payload associated with the event."""
class Send(TypedDict):
node: str
input: Optional[dict[str, Any]]
class Command(TypedDict, total=False):
send: Union[Send, Sequence[Send]]
update: dict[str, Any]
resume: Any
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.35"
version = "0.1.36"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
Generated
+4 -4
View File
@@ -2,13 +2,13 @@
[[package]]
name = "aiohappyeyeballs"
version = "2.4.2"
version = "2.4.3"
description = "Happy Eyeballs for asyncio"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohappyeyeballs-2.4.2-py3-none-any.whl", hash = "sha256:8522691d9a154ba1145b157d6d5c15e5c692527ce6a53c5e5f9876977f6dab2f"},
{file = "aiohappyeyeballs-2.4.2.tar.gz", hash = "sha256:4ca893e6c5c1f5bf3888b04cb5a3bee24995398efef6e0b9f747b5e89d84fd74"},
{file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"},
{file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"},
]
[[package]]
@@ -7331,4 +7331,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "67675531a6c4cd218c9effe87061234fbc778ff1afbb61327239b20556aa63da"
content-hash = "738e69cf406b140217cc8c3c0f2ccb5c2027d4701bcf2a347ba4b107f700ab2a"
+1
View File
@@ -8,6 +8,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
aiohappyeyeballs = "2.4.3"
[tool.poetry.group.docs.dependencies]
langgraph = { path = "libs/langgraph/", develop = true }