Compare commits

...
Author SHA1 Message Date
Nuno Campos af666a87fb sqlite 1.0.2 2024-09-02 17:58:01 -07:00
Nuno Campos 6e9f54e5ae postgres 1.0.6 2024-09-02 17:57:55 -07:00
Nuno Campos dd4a15b569 Require latest version of langgraph-checkpoint 2024-09-02 17:57:33 -07:00
Nuno CamposandGitHub 1122e5cbbb Merge pull request #1579 from langchain-ai/nc/2sep/async-checkpoint-test-setup
test: Ensure that async checkpointers in tests are setup in same event loop as they are used in
2024-09-02 17:57:05 -07:00
Nuno CamposandGitHub 29311f054d Merge pull request #1580 from langchain-ai/nc/2sep/async-checkpointers-implement-async
checkpoint: Implement sync methods in async checkpointers
2024-09-02 17:51:52 -07:00
Nuno Campos 3b2a237deb Lint 2024-09-02 17:45:57 -07:00
Nuno Campos 0cc3799298 checkpoint: Implement sync methods in async checkpointers
- These are needed when subgraphs are invoked synchronously in nodes
2024-09-02 17:43:34 -07:00
Nuno Campos ddeb9d7314 Disable test in old versions of py 2024-09-02 17:42:37 -07:00
Nuno Campos d65d0ec665 test: Ensure that async checkpointers in tests are setup in same event loop as they are used in
- This more closely remembers the environment they're used in, so it's what we should be testing
- Remove unnecessary pytest-asyncio dependency, use anyio pytest plugin instead
- Convert remaining async tests using only memory checkpointer to use all existing ones
2024-09-02 17:24:54 -07:00
Nuno CamposandGitHub a937754132 lib 0.2.16 2024-09-01 12:15:20 -07:00
Reuben MandGitHub 8c698fc362 Return replan response in dict. Add Replan example (#1566) 2024-08-31 23:41:48 +00:00
gbaian10andGitHub 3a07fe2efb docs: fix agent_supervisor.ipynb (#1562)
fix #1561
2024-08-31 16:35:59 -07:00
Nuno CamposandGitHub 7d6a302858 Merge pull request #1514 from vermapratyush/patch-1
[docs]: Fix typo
2024-08-30 16:20:18 -07:00
Nuno CamposandGitHub d225ef074f Merge pull request #1487 from langchain-ai/isaac/fixcustomersupportbot
customer support bot doc nit
2024-08-30 16:14:50 -07:00
Nuno Campos eca419008c checkpoint 1.0.8 2024-08-30 16:10:57 -07:00
Nuno CamposandGitHub 76994f3c9a Merge pull request #1521 from langchain-ai/nc/28aug/pydantic-serialization
checkpoint: Avoid changes to pydantic models breaking deserialization while still relying on pydantic's type coercion
2024-08-30 15:56:43 -07:00
Nuno Campos ca4ebe1031 Update tests 2024-08-30 15:51:57 -07:00
Nuno CamposandGitHub 299179bc33 Merge pull request #1555 from langchain-ai/nc/30aug/detect-nested-graph-in-function-node
Detect subgraphs called inside function nodes
2024-08-30 15:19:40 -07:00
Nuno Campos 487fc8a8ca Detect subgraphs called inside function nodes 2024-08-30 15:13:56 -07:00
Vadym BardaandGitHub cf4de9a1a5 cli: remove jupyter from examples pyproject (#1554) 2024-08-30 21:45:35 +00:00
Nuno CamposandGitHub 921c366708 Merge pull request #1531 from langchain-ai/dependabot/pip/libs/langgraph/notebook-7.2.2
Bump notebook from 7.2.1 to 7.2.2 in /libs/langgraph
2024-08-30 14:34:45 -07:00
Nuno CamposandGitHub 243881ae90 Merge pull request #1532 from langchain-ai/dependabot/pip/libs/langgraph/jupyterlab-4.2.5
Bump jupyterlab from 4.2.2 to 4.2.5 in /libs/langgraph
2024-08-30 14:34:26 -07:00
Nuno Campos 770132dd79 sdk-py: Add values to get/list threads 2024-08-30 14:23:10 -07:00
Nuno Campos 3943637202 Bump them all 2024-08-30 14:18:35 -07:00
Nuno CamposandGitHub 13cf9b7ec5 Merge pull request #1552 from langchain-ai/nc/30aug/sort-pending-writes-on-fetch
postgres/sqlite: Sort pending writes when fetching from db
2024-08-30 14:12:54 -07:00
Nuno Campos e744db169a postgres/sqlite: Sort pending writes when fetching from db 2024-08-30 14:07:46 -07:00
Nuno CamposandGitHub 770c092b57 Merge pull request #1551 from langchain-ai/nc/30aug/rm-none-default-node-inputs
Remove None default for missing keys when generating input for each node
2024-08-30 14:06:51 -07:00
Nuno Campos c5c10a41e5 Remove None default for missing keys when generating input for each node 2024-08-30 13:59:36 -07:00
Nuno Campos af1f218e00 Try init, then construct 2024-08-30 13:35:33 -07:00
Nuno CamposandGitHub 35807068ef Merge pull request #1545 from langchain-ai/nc/29aug/input-metadata-from-writes
For input checkpoint use filtered input writes in checkpoint metadata
2024-08-30 13:30:54 -07:00
Nuno Campos 5db13dffbe checkpoint: Avoid changes to pydantic models breaking deserialization while still relying on pydantic's type coercion 2024-08-30 13:28:32 -07:00
Nuno Campos 6379cdc36d Fix tests 2024-08-30 13:25:35 -07:00
Nuno CamposandNuno Campos 8154ab784e For input checkpoint use filtered input writes in checkpoint metadata
- if we use the raw input we can end up saving keys that were actually ignored (because not one of the input keys), which is incosistent
2024-08-30 13:24:41 -07:00
Vadym BardaandGitHub c761b80970 docs: update subgraph how-to (#1520) 2024-08-30 14:41:29 -04:00
Nuno CamposandGitHub 36d1570c0f Merge pull request #1550 from langchain-ai/nc/30aug/replay-without-fork
Remove the need to fork a thread to replay a past state
2024-08-30 10:39:57 -07:00
Nuno CamposandGitHub 99a845128e Merge pull request #1522 from langchain-ai/nc/28aug/sdk-join-return-val
sdk-py: Update signature for runs.join
2024-08-30 10:36:15 -07:00
Nuno Campos 6036b4a7d4 Remove the need to fork a thread to replay a past state 2024-08-30 10:35:16 -07:00
Nuno Campos 33edc8e808 sdk-py: Update signature for runs.join 2024-08-30 10:31:33 -07:00
Nuno CamposandGitHub 15a204b2be Merge pull request #1539 from langchain-ai/nc/29aug/nested-update-state
Implement update_state for nested graphs
2024-08-30 10:30:40 -07:00
Nuno CamposandGitHub 24a7e6403f Merge pull request #1538 from langchain-ai/nc/29aug/nested-replay
Replay a nested checkpoint
2024-08-30 10:30:14 -07:00
Nuno Campos 9fd64cd37b Lint 2024-08-30 10:19:20 -07:00
Nuno Campos fcd0f094f9 Add test from harrison, always produce output when resuming graph 2024-08-30 10:10:51 -07:00
Vadym BardaandGitHub 30598cd913 docs: fix images in structured react agent (#1544) 2024-08-30 01:50:14 +00:00
Nuno Campos 44153e4a49 Update test 2024-08-29 16:52:50 -07:00
Nuno Campos 4133910670 Implement update_state for nested graphs 2024-08-29 16:41:47 -07:00
Nuno Campos 4fc4080597 Enable test for doubly nested interrupts 2024-08-29 15:49:43 -07:00
Nuno Campos 8865831104 Update async test 2024-08-29 15:42:29 -07:00
Nuno Campos a707eef6f7 Lint 2024-08-29 15:42:25 -07:00
Nuno Campos 090c759da0 Replay a nested checkpoint
- note this isn't very useful until update_state is added
2024-08-29 15:39:14 -07:00
Nuno CamposandGitHub 4ebaaba282 Merge pull request #1499 from langchain-ai/nc/27aug/print-output-skipped-tasks
Emit output for skipped tasks, with `cached` mark
2024-08-29 15:38:35 -07:00
Isaac FranciscoandGitHub 5343350a5c [docs]: conceptual guides for streaming from specific nodes/LLMs (#1537)
* x

* changes

* typo
2024-08-29 22:09:18 +00:00
Nuno CamposandGitHub 5dcb5976d9 Merge pull request #1536 from RafaelRViana/documentation-fix
[docs]: fix parameter "multitask_strategy" misspelled
2024-08-29 14:03:52 -07:00
Nuno Campos 791ab654cb Add cached mark 2024-08-29 13:57:37 -07:00
Nuno Campos c183f4f16a Emit output for skipped tasks 2024-08-29 13:56:55 -07:00
Rafael Viana 11e8728685 [docs]: fix parameter misspelled 2024-08-29 17:49:04 -03:00
Nuno CamposandGitHub b1f22d3d81 Merge pull request #1534 from langchain-ai/nc/29aug/stream-subgraphs
Stream output from subgraphs
2024-08-29 13:37:13 -07:00
Nuno Campos ddf67d9233 Add ns to subgraph stream events 2024-08-29 13:32:21 -07:00
Nuno Campos b479b88c7e Add name of subgraph to streaming output 2024-08-29 13:20:28 -07:00
Isaac FranciscoandGitHub dd52248ef2 [docs]: make react agent HIL guide a little better (#1526)
* ocd

* x

* spelling

* changes

* edits

* nits

* changes

* last nit
2024-08-29 19:02:59 +00:00
Nuno Campos 80e442e13d Stream output from subgraphs
- enabled by new argument stream(subgraphs=True)
- the same stream_mode requested for parent graph is applied to all subgraphs
2024-08-29 12:00:02 -07:00
Nuno CamposandGitHub a627a30f4c Merge pull request #1530 from langchain-ai/nc/29aug/get-state-subgraph
Enable passing subgraph checkpoint config to get_state/aget_state
2024-08-29 11:59:02 -07:00
Vadym BardaandGitHub 01f14d3166 docs: update how-to for streaming from final node (#1533) 2024-08-29 18:28:22 +00:00
dependabot[bot]andGitHub 209e4cef7b Bump jupyterlab from 4.2.2 to 4.2.5 in /libs/langgraph
Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 4.2.2 to 4.2.5.
- [Release notes](https://github.com/jupyterlab/jupyterlab/releases)
- [Changelog](https://github.com/jupyterlab/jupyterlab/blob/@jupyterlab/lsp@4.2.5/CHANGELOG.md)
- [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/lsp@4.2.2...@jupyterlab/lsp@4.2.5)

---
updated-dependencies:
- dependency-name: jupyterlab
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-29 18:10:05 +00:00
dependabot[bot]andGitHub 98789acacc Bump notebook from 7.2.1 to 7.2.2 in /libs/langgraph
Bumps [notebook](https://github.com/jupyter/notebook) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/jupyter/notebook/releases)
- [Changelog](https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.2.2/CHANGELOG.md)
- [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.2.1...@jupyter-notebook/tree@7.2.2)

---
updated-dependencies:
- dependency-name: notebook
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-29 18:05:50 +00:00
Nuno Campos 9d68aac76f Enable passing subgraph checkpoint config to get_state/aget_state 2024-08-29 10:47:50 -07:00
Nuno CamposandGitHub e526674172 Merge pull request #1502 from langchain-ai/nc/27aug/subgraphs-cleanup
Nc/27aug/subgraphs cleanup
2024-08-29 10:17:04 -07:00
Nuno Campos ca8614f873 Implement get_state and get_state_history for nested graphs 2024-08-29 09:45:45 -07:00
ae8b1e164b docs: expand streaming conceptual guides (#1524)
* stash

* cr

* x

* spelling

* Update docs/docs/cloud/how-tos/stream_events.md

Co-authored-by: Vadym Barda <vadym@langchain.dev>

* Update docs/docs/cloud/how-tos/stream_updates.md

Co-authored-by: Vadym Barda <vadym@langchain.dev>

* Update docs/docs/cloud/how-tos/stream_values.md

Co-authored-by: Vadym Barda <vadym@langchain.dev>

* Update docs/docs/concepts/low_level.md

Co-authored-by: Vadym Barda <vadym@langchain.dev>

* final nits

* spelling

---------

Co-authored-by: isaac hershenson <ihershenson@hmc.edu>
Co-authored-by: Isaac Francisco <78627776+isahers1@users.noreply.github.com>
Co-authored-by: Vadym Barda <vadym@langchain.dev>
2024-08-29 15:03:08 +00:00
gbaian10andGitHub cd28799215 fix: ToolNode output isn't a str and contains non-ASCII characters (#1505) 2024-08-29 09:57:23 -04:00
Harrison ChaseandGitHub 883b39ba9f docs: better name for react agent w/ structured output how-to (#1527) 2024-08-29 13:50:01 +00:00
Niklas MartinandGitHub 337ccc058f [docs]: fix type hint for state (#1528) 2024-08-29 13:47:05 +00:00
Isaac FranciscoandGitHub 6111d37b99 [docs]: different structured output method (#1497)
* wip

* spelling

* consistent headers

* slight changes

* spelling

* more comments

* link

* changes

* diagrams

* format

* final nits

* x
2024-08-28 20:45:23 -07:00
Isaac FranciscoandGitHub 6962aa5acc ocd (#1525) 2024-08-29 01:50:55 +00:00
Vadym BardaandGitHub 4769122828 docs: fix map-reduce issue (#1523) 2024-08-28 20:40:06 +00:00
Nuno Campos c4010c03a6 Udpate async tests 2024-08-28 12:50:35 -07:00
Nuno Campos 03f6cf779d Fix test setup 2024-08-28 12:48:29 -07:00
Nuno Campos 112700810c Fix 2024-08-28 12:31:43 -07:00
Isaac FranciscoandGitHub 2645eb5a48 [docs]: remove agent executor (#1485) 2024-08-28 14:54:52 -04:00
Isaac FranciscoandGitHub ab71d29286 [docs]: update node config info (#1500)
* config changes

* vadym comments

* format

* spelling

* x
2024-08-28 11:54:44 -07:00
Isaac FranciscoandGitHub da38905db7 [docs]: fix llm compiler (#1517) 2024-08-28 13:36:36 -04:00
Vadym BardaandGitHub 7d3527db38 cli: release 0.1.52 (#1516) 2024-08-28 12:12:46 -04:00
David DuongandGitHub 36ed1a5d4b Merge pull request #1503 from langchain-ai/dqbd/cli-studio-js
feat(cli): add building support for JS
2024-08-28 18:08:32 +02:00
Vadym BardaandGitHub 7637a769dd docs: update reflection notebook (#1515)
* docs: update reflection notebook

* update more

* update more
2024-08-28 12:07:29 -04:00
Tat Dat Duong 3bbe09dac4 Fix missing comma 2024-08-28 17:56:26 +02:00
Pratyush VermaandGitHub 8e6019a6b8 Merge branch 'main' into patch-1 2024-08-28 15:46:39 +01:00
Pratyush VermaandGitHub 666fb3f31d Fix typo 2024-08-28 15:45:02 +01:00
Vadym BardaandGitHub 947f8c8c1d docs: fix astream in reflection notebook (#1512) 2024-08-28 14:32:09 +00:00
Isaac FranciscoandGitHub 84cb3ea151 [docs]: recursion limit info (#1501)
* draft

* vadym comments

* spelling

* final nits

* final nits
2024-08-27 21:06:09 -07:00
Tat Dat Duong f4dea9c5f7 feat(cli): add building support for JS 2024-08-28 02:07:21 +02:00
Nuno Campos a8a2a89602 Fix docstring 2024-08-27 16:59:20 -07:00
Nuno Campos e338a4ec98 Fix filtering on ns 2024-08-27 16:09:11 -07:00
Nuno Campos 499fe10ef2 Undo addition of Send.id 2024-08-27 13:34:50 -07:00
Isaac FranciscoandGitHub a8758661bc acesss tool calls directly (#1495) 2024-08-27 14:47:20 -04:00
Nuno CamposandGitHub 8912d50312 Merge pull request #1498 from langchain-ai/nc/27aug/store-sends-once
Avoid storing Sends twice in memory and postgres checkpointers
2024-08-27 11:24:27 -07:00
Nuno Campos 8d23929288 Remove print 2024-08-27 11:18:53 -07:00
Nuno Campos 9b6100bc4f Avoid storing Sends twice in memory and postgres checkpointers
- Sends are stored through put_writes, so we don't need to also store them inside checkpoint object
- On reading checkpoint, reconstruct pending_sends from the stored writes
2024-08-27 11:10:18 -07:00
Nuno CamposandGitHub 238b562e78 Merge pull request #1496 from langchain-ai/nc/27aug/update-ruff
Update ruff
2024-08-27 11:06:43 -07:00
Nuno Campos 7fcc0da4e3 Reformat 2024-08-27 11:00:25 -07:00
Nuno Campos d7eff6ab85 Remove abc 2024-08-27 10:52:26 -07:00
Nuno Campos 330c91f622 Update ruff 2024-08-27 10:50:39 -07:00
3dbf6f144e [docs]: structured output from tool calling agent (#1480)
* first draft

* vadym comments

* Update docs/mkdocs.yml

Co-authored-by: Vadym Barda <vadym@langchain.dev>

* vadym comments

* spellign

* more comments

* spelling

* final nits

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
2024-08-27 10:26:28 -07:00
Nuno Campos 8c2fefcb28 sdk-py: Add values to Thread schema 2024-08-27 08:42:55 -07:00
Vadym BardaandGitHub c88d289a73 checkpoint-postgres: release 1.0.4 (#1493) 2024-08-27 11:34:26 -04:00
Vadym BardaandGitHub d12f5c6d8b checkpoint-postgres: allow passing pool (#1452)
* checkpoint-postgres: allow passing pool

* make psycopg_pool a non-dev dependency

* code review

* lockfile

* move methods

* relax requirements, remove binary

* add binary to dev dependencies

* update readme
2024-08-27 15:30:13 +00:00
9ff54e029e [docs]: conceptual guide for astream_events (#1482)
---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
2024-08-27 15:18:51 +00:00
Nuno CamposandGitHub 2a89c7a5b4 Merge pull request #1492 from langchain-ai/nc/27aug/all-value-channels-same-checkpoint-get
lib: For all value channels the return value of checkpoint() and get() are the same
2024-08-27 08:15:45 -07:00
Nuno Campos f2ec99b9a8 lib: For all value channels the return value of checkpoint() and get() are the same 2024-08-27 08:10:26 -07:00
Vadym BardaandGitHub eb387cc946 docs: update cloud time travel docs to pass checkpoint_id correctly (#1483) 2024-08-27 13:26:22 +00:00
Vadym BardaandGitHub 54b53f384d sdk-js: release 0.0.8 (#1490) 2024-08-27 09:11:57 -04:00
isaac hershenson 86c047eb31 changes 2024-08-26 18:05:17 -07:00
Nuno CamposandGitHub 65784d8174 Merge pull request #1108 from langchain-ai/vb/update-get-state
langgraph: update get_state to handle nested subgraph state
2024-08-26 17:58:41 -07:00
vbarda 507930e5b7 lint 2024-08-26 19:29:05 -04:00
vbarda 85e698e20b filter on checkpoint NS 2024-08-26 19:26:35 -04:00
vbarda bf4dc5d5f6 Merge branch 'main' into vb/update-get-state 2024-08-26 19:17:26 -04:00
Nuno CamposandGitHub ce0fad8ddb Merge pull request #1484 from langchain-ai/vb/add-checkpoint-id-js-sdk
sdk-js: add checkpoint_id arg to runs create/stream/wait payload
2024-08-26 15:36:03 -07:00
Nuno Campos 1315c0d743 sdk-py0.1.29 2024-08-26 15:27:53 -07:00
vbarda ada751d19b better comment 2024-08-26 18:25:43 -04:00
vbarda f310716921 sdk-js: add checkpoint_id arg to runs create/stream/wait payload 2024-08-26 17:16:06 -04:00
Nuno CamposandGitHub 568044171b Merge pull request #1479 from langchain-ai/nc/26aug/with-config
Override with_config to store config in Pregel instance
2024-08-26 13:22:30 -07:00
Nuno Campos 1d0f3577a7 Catch any type error when reviving saved values 2024-08-26 13:17:19 -07:00
Nuno Campos 3979bdb792 Type as self 2024-08-26 13:14:11 -07:00
Nuno CamposandGitHub d8d663ccd5 Merge pull request #1371 from langchain-ai/wfh/set_entry
Use START
2024-08-26 12:01:51 -07:00
Nuno Campos 25a72e77ef Override with_config to store config in Pregel instance
- This enables eg customizing callbacks/metadata in langgraph cloud deployments
2024-08-26 11:57:57 -07:00
Alexander KovriginandGitHub da806c466d Allow passing ToolNode as tools in create_react_agent (#1451) 2024-08-26 13:47:50 -04:00
Vadym BardaandGitHub 49c316578b langgraph: allow END end key in add_edge with list inputs (#1478) 2024-08-26 12:51:18 -04:00
Nuno CamposandGitHub 7039a54871 Merge pull request #1477 from langchain-ai/nc/26aug/update-docs-constraints
docs: Update langgraph-api/cloud version constraints
2024-08-26 08:34:55 -07:00
Nuno CamposandGitHub ef17e0351a Merge pull request #1457 from langchain-ai/dependabot/npm_and_yarn/libs/sdk-js/micromatch-4.0.8
Bump micromatch from 4.0.7 to 4.0.8 in /libs/sdk-js
2024-08-26 08:25:46 -07:00
Nuno Campos 0d9c0732d4 docs: Update langgraph-api/cloud version constraints 2024-08-26 08:25:24 -07:00
William FHandGitHub a2cfe694f1 Fix import (#1472) 2024-08-25 20:47:05 -07:00
Nuno CamposandGitHub 858c166cae Merge pull request #1463 from langchain-ai/nc/24aug/sdk-on-completion
sdk: Add on_completion param
2024-08-24 21:38:11 -07:00
Nuno Campos 03785c7d83 sdk: Add on_completion param 2024-08-24 21:34:35 -07:00
David DuongandGitHub 93cb2a7730 Merge pull request #1460 from langchain-ai/dqbd/js-api-key
feat(sdk-js): add apiKey property
2024-08-24 19:29:57 +02:00
Tat Dat Duong 4b48e71d2c Bump to 0.0.7 2024-08-24 19:24:39 +02:00
Tat Dat Duong b7744aff9b feat(sdk-js): add apiKey property 2024-08-24 19:24:13 +02:00
Nuno Campos 4a4dd16535 checkpoint 1.0.6 2024-08-23 18:25:56 -07:00
Nuno Campos ef790a57c6 Lint 2024-08-23 18:17:51 -07:00
Nuno Campos c49692d794 checkpoint 1.0.5 2024-08-23 18:15:48 -07:00
Nuno Campos 3cda14b069 lib 0.2.14 2024-08-23 18:15:31 -07:00
Nuno Campos 66a13b8865 Add test for runtime value replacement with pydantic model 2024-08-23 18:15:15 -07:00
Nuno Campos 58e139e7fd Lint 2024-08-23 18:15:04 -07:00
Nuno Campos a8d860273b Skip runtime value replacement when not needed 2024-08-23 18:14:59 -07:00
Nuno Campos 3ac4cdf3d4 Fix pydantic model deserialization 2024-08-23 18:14:40 -07:00
Nuno Campos 3a524e0e56 Fix attributeerror 2024-08-23 17:55:14 -07:00
Andrew NguonlyandGitHub 134f8faf8c Add section about authentication to API reference. (#1458) 2024-08-23 17:14:16 -07:00
vbarda 904a1a3471 extra paranoia 2024-08-23 18:09:10 -04:00
vbarda cb30f68642 remove more reused code 2024-08-23 18:01:35 -04:00
vbarda 15692acef9 refactor to remove nested DB calls 2024-08-23 17:56:28 -04:00
dependabot[bot]andGitHub 610257665f Bump micromatch from 4.0.7 to 4.0.8 in /libs/sdk-js
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-23 20:36:31 +00:00
Nuno Campos bc482431c3 lib0.2.13 2024-08-23 13:35:21 -07:00
155e0c66d5 docs: add how-to for dynamic interrupts (#1446)
---------

Co-authored-by: vbarda <vadym@langchain.dev>
2024-08-23 20:34:33 +00:00
Nuno CamposandGitHub ca63a06549 Merge pull request #1455 from langchain-ai/nc/23aug/some-magic-for-will
lib: Context values never stored in checkpoints
2024-08-23 13:17:50 -07:00
Nuno Campos e8c553c41e Lint 2024-08-23 13:12:46 -07:00
Nuno Campos beafddf7c8 Lint 2024-08-23 13:09:28 -07:00
1e6da19257 Update libs/langgraph/langgraph/managed/context.py
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2024-08-23 13:08:56 -07:00
Nuno Campos e0898409b9 Lint 2024-08-23 12:57:31 -07:00
Nuno Campos bc86757e73 lib: Context values never stored in checkpoints
- Convert Context to a ManagedValue
- Add shim for old Context constructor
- Add `runtime` flag for managed values, which, prior to serialization, replaces the value with a placeholder, and replaces it back with the actual value on resuming from checkpoint
2024-08-23 12:46:31 -07:00
vbarda 71442916e5 lint 2024-08-23 14:52:31 -04:00
vbarda 1f29925034 add max recursion depth 2024-08-23 14:44:42 -04:00
Vadym BardaandGitHub 4c4d7054ab Merge branch 'main' into vb/update-get-state 2024-08-23 14:27:40 -04:00
vbarda 1333d8b478 cleanup 2024-08-23 14:26:54 -04:00
vbarda 2bac0d0f41 re-trigger CI 2024-08-23 13:51:57 -04:00
David DuongandGitHub ed7b2c9e8a Merge pull request #1443 from langchain-ai:dqbd/js-sdk-end-event
fix(sdk-js): support sending end events
2024-08-23 16:48:02 +02:00
Nuno CamposandGitHub 0597aedaff Merge pull request #1448 from langchain-ai/nc/22aug/serde-exceptions
Don't try to serialize exceptions
2024-08-22 21:41:06 -07:00
Nuno Campos 82db383199 Don't try to serialize exceptions
- Store their string repr instead
2024-08-22 21:36:27 -07:00
William FHandGitHub dec7eb6f58 [Docs] Use injected RunnableConfig (#1444)
* Pass via type

* Format
2024-08-22 18:54:42 -07:00
vbarda 065055e587 small change 2024-08-22 20:37:48 -04:00
vbarda 4162be8119 optimize subgraph state lookups 2024-08-22 20:35:08 -04:00
Nuno Campos 6ece7124ed Fix param name in docstring 2024-08-22 17:21:01 -07:00
Nuno CamposandGitHub ffa9b8672a Merge pull request #1445 from langchain-ai/nc/22aug/sdk-on-disconnect
sdk: Add on_disconnect arg to create/wait streaming run
2024-08-22 17:06:24 -07:00
Nuno Campos 19b382335f sdk: Add on_disconnect arg to create/wait streaming run 2024-08-22 17:03:11 -07:00
Tat Dat Duong c72acc9145 Add missing status 2024-08-23 01:38:53 +02:00
Nuno CamposandGitHub c30aa1ca13 Merge pull request #1337 from langchain-ai/nc/13aug/sdk-py-join-stream
sdk-py: Add Runs.join_stream endpoint
2024-08-22 15:38:23 -07:00
Tat Dat Duong 0b7f451b40 Bump to 0.0.6 2024-08-23 00:18:28 +02:00
Tat Dat Duong 75dec9b924 fix(sdk-js): support sending end events 2024-08-23 00:18:06 +02:00
vbarda 9f6e57d2a7 more code review 2024-08-22 17:37:46 -04:00
vbarda 72893d9abb code review 2024-08-22 17:33:17 -04:00
vbarda 4935cf52bf lint 2024-08-22 16:52:16 -04:00
vbarda acd8acf237 lint 2024-08-22 16:47:12 -04:00
vbarda 6c7d9c35bc remove interrupts 2024-08-22 15:37:20 -04:00
vbarda 5654d8fc86 Merge branch 'main' into vb/update-get-state 2024-08-22 15:34:14 -04:00
Vadym BardaandGitHub 426125cf96 Merge branch 'main' into vb/update-get-state 2024-08-21 19:20:20 -04:00
vbarda 0b6088f913 remove futures.clear 2024-08-21 18:26:54 -04:00
vbarda 578ec48031 Merge branch 'main' into vb/update-get-state 2024-08-21 18:21:28 -04:00
vbarda e7bc74e918 remove refactors 2024-08-21 18:06:18 -04:00
vbarda 0a87b9fa1c update more tests 2024-08-21 17:49:51 -04:00
vbarda f51e7ea9a4 pass pending writes in checkpointers 2024-08-21 15:38:30 -04:00
vbarda a94168af7f Merge branch 'main' into vb/update-get-state 2024-08-21 15:07:52 -04:00
William Fu-Hinthorn 1bd40b2ebf Do markdown 2024-08-16 15:45:56 -07:00
William Fu-Hinthorn b5429b6342 Use START 2024-08-16 15:43:24 -07:00
vbarda 45054df71a remove include_subgraph_state kwarg 2024-08-14 21:07:56 -04:00
vbarda 7fa97898aa correctly propagate all subgraph attributes 2024-08-14 16:15:08 -04:00
vbarda 6531ec7669 remove inherit checkpointer 2024-08-14 14:14:06 -04:00
vbarda 409b915a3f code review 2024-08-14 12:10:07 -04:00
Nuno Campos 3b56cdf524 Add Runs.join_stream endpoint 2024-08-13 17:35:31 -07:00
vbarda d9618880a3 update logic for latest snapshot's subgraph snapshots 2024-08-13 20:13:13 -04:00
Vadym BardaandGitHub 392891f5fc Merge branch 'main' into vb/update-get-state 2024-08-13 18:20:32 -04:00
vbarda 58887a5a3b checkpoints/interrupts for subgraphs triggered by sends 2024-08-13 18:15:57 -04:00
vbarda 3295274711 fix empty snapshot 2024-08-13 10:23:53 -04:00
vbarda 0135c6f743 correct check for using parent checkpointer 2024-08-13 10:07:52 -04:00
Vadym BardaandGitHub 0456f52195 Merge branch 'main' into vb/update-get-state 2024-08-12 20:30:56 -04:00
vbarda b5caf1ad3f Merge branch 'vb/update-get-state' of github.com:langchain-ai/langgraph into vb/update-get-state 2024-08-12 20:29:15 -04:00
vbarda f65d9b2b7d pass subgraph nodes/channels 2024-08-12 20:29:09 -04:00
Vadym BardaandGitHub a03886b9e4 Merge branch 'main' into vb/update-get-state 2024-08-12 16:53:18 -04:00
vbarda abe9b7c08e lint 2024-08-12 16:32:22 -04:00
vbarda 6d4cdc9456 opt-in 2024-08-12 16:29:15 -04:00
vbarda 5033044587 update checkpointer tests 2024-08-12 16:22:27 -04:00
vbarda 246dd0f8c0 Merge branch 'vb/update-get-state' of github.com:langchain-ai/langgraph into vb/update-get-state 2024-08-12 15:52:33 -04:00
vbarda fb05bdc2bf return all checkpoints from .list 2024-08-12 15:52:17 -04:00
vbarda 9948125745 add checkpointer=INHERIT_CHECKPOINTER 2024-08-12 14:27:50 -04:00
vbarda 322cfc46d3 cleanup 2024-08-12 12:43:28 -04:00
vbarda c9d6a41d75 Merge branch 'main' into vb/update-get-state 2024-08-12 10:26:56 -04:00
Vadym BardaandGitHub 56bf9c9047 Merge branch 'main' into vb/update-get-state 2024-07-24 14:03:10 -04:00
vbarda a7d48465da use .list for looking up prefix-matched checkpoints 2024-07-24 13:45:48 -04:00
vbarda 6ae2c6c7b9 Merge branch 'main' into vb/update-get-state 2024-07-24 11:18:10 -04:00
vbarda ae696d4f30 add sync history 2024-07-23 21:40:04 -04:00
vbarda b43ef6440f tests 2024-07-23 20:59:10 -04:00
vbarda e615aabf14 cleanup names + make subgraph state optional 2024-07-23 20:28:23 -04:00
vbarda 92ae8f4817 async methods 2024-07-23 19:51:44 -04:00
vbarda 2268802908 Merge branch 'main' into vb/update-get-state 2024-07-23 18:51:36 -04:00
vbarda 261cdf88a5 langgraph: update get_state to handle nested subgraph state 2024-07-23 18:35:49 -04:00
166 changed files with 16642 additions and 11447 deletions
+2 -2
View File
@@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph, MessagesState
from langgraph.graph import END, START, StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
@@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
workflow.add_edge(START, "agent")
# We now add a conditional edge
workflow.add_conditional_edges(
+4
View File
@@ -38,6 +38,7 @@ _MANUAL = {
"visualization.ipynb",
"state-model.ipynb",
"subgraph.ipynb",
"recursion-limit.ipynb",
"force-calling-a-tool-first.ipynb",
"pass-run-time-values-to-tools.ipynb",
"tool-calling.ipynb",
@@ -56,11 +57,14 @@ _MANUAL = {
"create-react-agent-memory.ipynb",
"create-react-agent-hitl.ipynb",
"human_in_the_loop/breakpoints.ipynb",
"human_in_the_loop/dynamic_breakpoints.ipynb",
"human_in_the_loop/time-travel.ipynb",
"human_in_the_loop/edit-graph-state.ipynb",
"human_in_the_loop/wait-user-input.ipynb",
"human_in_the_loop/review-tool-calls.ipynb",
"node-retries.ipynb",
"react_diagrams.png",
"react-agent-structured-output.ipynb",
],
"tutorials": [
"introduction.ipynb",
+95 -3
View File
@@ -51,16 +51,108 @@ The LangGraph Cloud API offers several features to support complex agent archite
Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. The LangGraph Cloud API supports five streaming modes.
- `values`: Stream the full state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values.
- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values.
- `messages`: Stream complete messages (at the end of node execution) as well as tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. This is only an option if your graph contains a `messages` key. See the [how-to guide](../how-tos/stream_messages.md) for streaming messages.
- `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_updates.md) for streaming updates.
- `events`: Stream all events (including the state of the graph) after each node is executed. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs.
- `debug`: Stream debug events after each node is executed. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events.
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs.
- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events.
You can also specify multiple streaming modes at the same time. See the [how-to guide](../how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream) for how to create streaming runs.
Streaming modes `values`, `updates`, and `debug` are very similar to modes available in the LangGraph library - for a deeper conceptual explanation of those, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming).
Streaming mode `events` is the same as using `.astream_events` in the LangGraph library - for a deeper conceptual explanation of this, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming).
#### `mode="messages"`
Streaming mode `messages` is a new streaming mode, currently only available in the API. What does this mode enable?
This mode is focused on streaming back messages. It currently assumes that you have a `messages` key in your graph that is a list of messages. Assuming we have a simple react agent deployed, what does this stream look like?
All events emitted have two attributes:
- `event`: This is the name of the event
- `data`: This is data associated with the event
Let's run it on a question that should trigger a tool call:
```python
thread = await client.threads.create()
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
events = []
async for event in client.runs.stream(
thread["thread_id"],
assistant_id="agent", # This may need to change depending on the graph you deployed
input=input,
stream_mode="messages",
):
print(event.event)
```
```shell
metadata
messages/complete
messages/metadata
messages/partial
...
messages/partial
messages/complete
messages/complete
messages/metadata
messages/partial
...
messages/partial
messages/complete
end
```
We first get some `metadata` - this is metadata about the run.
```python
StreamPart(event='metadata', data={'run_id': '1ef657cf-ae55-6f65-97d4-f4ed1dbdabc6'})
```
We then get a `messages/complete` event - this a fully formed message getting emitted. In this case,
this was the just the input message we sent in.
```python
StreamPart(event='messages/complete', data=[{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '833c09a3-bb19-46c9-81d9-1e5954ec5f92', 'example': False}])
```
We then get a `messages/metadata` - this is just letting us know that a new message is starting.
```python
StreamPart(event='messages/metadata', data={'run-985c0f14-9f43-40d4-a505-4637fc58e333': {'metadata': {'created_by': 'system', 'run_id': '1ef657de-7594-66df-8eb2-31518e4a1ee2', 'graph_id': 'agent', 'thread_id': 'c178eab5-e293-423c-8e7d-1d113ffe7cd9', 'model_name': 'openai', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_provider': 'openai', 'ls_model_name': 'gpt-4o', 'ls_model_type': 'chat', 'ls_temperature': 0.0}}})
```
We then get a BUNCH of `messages/partial` events - these are the individual tokens from the LLM! In the case below, we can see the START of a tool call.
```python
StreamPart(event='messages/partial', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [{'name': 'tavily_search_results_json', 'args': '', 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'error': None}], 'usage_metadata': None}])
```
After that, we get a `messages/complete` event - this is the AIMessage finishing. It's now a complete tool call:
```python
StreamPart(event='messages/complete', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '{"query":"current weather in San Francisco"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}], 'invalid_tool_calls': [], 'usage_metadata': None}])
```
After that, we get ANOTHER `messages/complete` event. This is a tool message - our agent has called a tool, gotten a response, and now inserting it into the state in the form of a tool message.
```python
StreamPart(event='messages/complete', data=[{'content': '[{"url": "https://www.weatherapi.com/", "content": "{\'location\': {\'name\': \'San Francisco\', \'region\': \'California\', \'country\': \'United States of America\', \'lat\': 37.78, \'lon\': -122.42, \'tz_id\': \'America/Los_Angeles\', \'localtime_epoch\': 1724877689, \'localtime\': \'2024-08-28 13:41\'}, \'current\': {\'last_updated_epoch\': 1724877000, \'last_updated\': \'2024-08-28 13:30\', \'temp_c\': 23.3, \'temp_f\': 73.9, \'is_day\': 1, \'condition\': {\'text\': \'Partly cloudy\', \'icon\': \'//cdn.weatherapi.com/weather/64x64/day/116.png\', \'code\': 1003}, \'wind_mph\': 15.0, \'wind_kph\': 24.1, \'wind_degree\': 310, \'wind_dir\': \'NW\', \'pressure_mb\': 1014.0, \'pressure_in\': 29.93, \'precip_mm\': 0.0, \'precip_in\': 0.0, \'humidity\': 57, \'cloud\': 25, \'feelslike_c\': 25.0, \'feelslike_f\': 77.1, \'windchill_c\': 20.9, \'windchill_f\': 69.6, \'heatindex_c\': 23.3, \'heatindex_f\': 74.0, \'dewpoint_c\': 12.9, \'dewpoint_f\': 55.2, \'vis_km\': 16.0, \'vis_miles\': 9.0, \'uv\': 6.0, \'gust_mph\': 19.5, \'gust_kph\': 31.3}}"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '0112eba5-7660-4375-9f24-c7a1d6777b97', 'tool_call_id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}])
```
After that, we see the agent doing another LLM call and streaming back a response. We then get an `end` event:
```python
StreamPart(event='end', data=None)
```
And that's it! This is more focused streaming mode specifically focused on streaming back messages. See this [how-to guide](../how-tos/stream_messages.md) for more information.
### Human-in-the-Loop
There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in the [human in the loop how-tos](../how-tos/index.md#human-in-the-loop).
+5 -5
View File
@@ -28,7 +28,7 @@ In the standard LangGraph API configuration, the server uses the compiled graph
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
from langgraph.graph import END, START, MessageGraph
model = ChatOpenAI(temperature=0)
@@ -36,7 +36,7 @@ graph_workflow = MessageGraph()
graph_workflow.add_node("agent", model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
graph_workflow.add_edge(START, "agent")
agent = graph_workflow.compile()
```
@@ -60,7 +60,7 @@ To make your graph rebuild on each new run with custom configuration, you need t
```python
from typing import Annotated, TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
from langgraph.graph import END, START, MessageGraph
from langgraph.graph.state import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
@@ -83,7 +83,7 @@ def make_default_graph():
graph_workflow.add_node("agent", call_model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
graph_workflow.add_edge(START, "agent")
agent = graph_workflow.compile()
return agent
@@ -113,7 +113,7 @@ def make_alternative_graph():
graph_workflow.add_node("agent", call_model)
graph_workflow.add_node("tools", tool_node)
graph_workflow.add_edge("tools", "agent")
graph_workflow.set_entry_point("agent")
graph_workflow.add_edge(START, "agent")
graph_workflow.add_conditional_edges("agent", should_continue)
agent = graph_workflow.compile()
+24 -18
View File
@@ -1,15 +1,14 @@
# How to Set Up a LangGraph Application for Deployment
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies.
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies.
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment.
!!! tip "Setup with pyproject.toml"
If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud.
If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud.
!!! tip "Setup with a Monorepo"
If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so.
If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so.
The final repo structure will look something like this:
@@ -35,23 +34,27 @@ After each step, an example file directory is provided to demonstrate how code c
Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.2.0,<0.3.0
langgraph>=0.2.7,<0.3.0
langgraph-checkpoint>=1.0.4
langchain-core>=0.2.27,<0.3.0
langsmith>=0.1.63
orjson>=3.10.1
httpx>=0.27.0
tenacity>=8.3.0
uvicorn>=0.29.0
orjson>=3.9.7
httpx>=0.25.0
tenacity>=8.0.0
uvicorn>=0.26.0
sse-starlette>=2.1.0
uvloop>=0.19.0
httptools>=0.6.1
jsonschema-rs>=0.18.0
uvloop>=0.18.0
httptools>=0.5.0
jsonschema-rs>=0.16.3
croniter>=1.0.1
structlog>=24.4.0
structlog>=23.1.0
redis>=5.0.0,<6.0.0
```
Example `requirements.txt` file:
```
langgraph
langchain_anthropic
@@ -62,6 +65,7 @@ langchain_openai
```
Example file directory:
```bash
my-app/
├── my_agent # all project code lies within here
@@ -73,6 +77,7 @@ my-app/
Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment.
Example `.env` file:
```
MY_ENV_VAR_1=foo
MY_ENV_VAR_2=bar
@@ -94,12 +99,11 @@ Implement your graphs! Graphs can be defined in a single file or multiple files.
Example `agent.py` file, which shows how to import from other modules you define (code for the modules is not shown here, please see [this repo](https://github.com/langchain-ai/langgraph-example) to see their implementation):
```python
# my_agent/agent.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph import StateGraph, END, START
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
from my_agent.utils.state import AgentState # import state
@@ -110,7 +114,7 @@ class GraphConfig(TypedDict):
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.set_entry_point("agent")
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
@@ -125,9 +129,10 @@ graph = workflow.compile()
```
!!! warning "Assign `CompiledGraph` to Variable"
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
Example file directory:
```bash
my-app/
├── my_agent # all project code lies within here
@@ -147,6 +152,7 @@ my-app/
Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file.
Example `langgraph.json` file:
```json
{
"dependencies": ["./my_agent"],
@@ -160,7 +166,7 @@ Example `langgraph.json` file:
Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:<variable_name>`).
!!! warning "Configuration Location"
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies.
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies.
Example file directory:
+12 -10
View File
@@ -35,19 +35,21 @@ Dependencies can optionally be specified in one of the following files: `pyproje
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.2.0,<0.3.0
langgraph>=0.2.7,<0.3.0
langgraph-checkpoint>=1.0.4
langchain-core>=0.2.27,<0.3.0
langsmith>=0.1.63
orjson>=3.10.1
httpx>=0.27.0
tenacity>=8.3.0
uvicorn>=0.29.0
orjson>=3.9.7
httpx>=0.25.0
tenacity>=8.0.0
uvicorn>=0.26.0
sse-starlette>=2.1.0
uvloop>=0.19.0
httptools>=0.6.1
jsonschema-rs>=0.18.0
uvloop>=0.18.0
httptools>=0.5.0
jsonschema-rs>=0.16.3
croniter>=1.0.1
structlog>=24.4.0
redis>=5.0.8,<6.0.0
```
Example `pyproject.toml` file:
@@ -109,7 +111,7 @@ Example `agent.py` file, which shows how to import from other modules you define
# my_agent/agent.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph import StateGraph, END, START
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
from my_agent.utils.state import AgentState # import state
@@ -120,7 +122,7 @@ class GraphConfig(TypedDict):
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.set_entry_point("agent")
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
@@ -34,7 +34,8 @@ First, we need to setup our client so that we can communicate with our hosted gr
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
--header 'Content-Type: application/json' \
--data {}
```
## Replay a state
@@ -46,7 +47,7 @@ Before replaying a state - we need to create states to replay from! In order to
=== "Python"
```python
input = { 'messages':[{ "role":"user", "content":"Please search the weather in SF" }] }
input = {"messages": [{"role": "user", "content": "Please search the weather in SF"}]}
async for chunk in client.runs.stream(
thread["thread_id"],
@@ -61,7 +62,7 @@ Before replaying a state - we need to create states to replay from! In order to
=== "Javascript"
```js
const input = {"messages": [{ "role": "human", "content": "Please search the weather in SF"}] }
const input = { "messages": [{ "role": "human", "content": "Please search the weather in SF" }] }
const streamResponse = client.runs.stream(
thread["thread_id"],
@@ -155,17 +156,23 @@ Output:
To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows:
To rerun from a state, we need first issue an empty update to the thread state. Then we need to pass in the resulting `checkpoint_id` as follows:
=== "Python"
```python
state_to_replay = states[2]
updated_config = await client.threads.update_state(
thread["thread_id"],
{"messages": []},
checkpoint_id=state_to_replay["checkpoint_id"]
)
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id, # graph_id
input=None,
stream_mode="updates",
config={"configurable": {"checkpoint_id": state_to_replay['checkpoint_id']}}
checkpoint_id=updated_config["checkpoint_id"]
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
@@ -174,13 +181,15 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of
=== "Javascript"
```js
const stateToReplay = states[2];
const config = await client.threads.updateState(thread["thread_id"], { values: {"messages": [] }, checkpointId: stateToReplay["checkpoint_id"] });
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: null,
streamMode: "updates",
config: {"configurable": {"checkpoint_id": stateToReplay['checkpoint_id']}},
checkpointId: config["checkpoint_id"]
}
);
for await (const chunk of streamResponse) {
@@ -193,38 +202,46 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of
=== "CURL"
```bash
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -r '.[2].checkpoint_id' | {
read checkpoint_id
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"config\": {\"configurable\": {\"checkpoint_id\": \"$checkpoint_id\"}},
\"stream_mode\": [
\"updates\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
sub(/^event: /, "", $0)
event_type = $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -c '
.[2] as $state_to_replay |
{
values: { messages: .[2].values.messages[-1] },
checkpoint_id: $state_to_replay.checkpoint_id
}' | \
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
--header 'Content-Type: application/json' \
--data @- | jq .checkpoint_id | \
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"checkpoint_id\": \"$1\",
\"stream_mode\": [
\"updates\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
'
sub(/^event: /, "", $0)
event_type = $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
}
'
```
Output:
@@ -251,7 +268,7 @@ Let's show how to do this to edit the state at a particular point in time. Let's
# Let's now update the args for that tool call
last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'}
new_state = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])
config = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])
```
=== "Javascript"
@@ -262,9 +279,9 @@ Let's show how to do this to edit the state at a particular point in time. Let's
let lastMessage = stateToReplay['values']['messages'][-1];
// Let's now update the args for that tool call
lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in SF'};
lastMessage['tool_calls'][0]['args'] = { 'query': 'current weather in SF' };
const newState = await client.threads.updateState(thread['thread_id'],{values:{"messages":[lastMessage]},checkpointId:stateToReplay['checkpoint_id']});
const config = await client.threads.updateState(thread['thread_id'], { values: { "messages": [lastMessage] }, checkpointId: stateToReplay['checkpoint_id'] });
```
=== "CURL"
@@ -294,7 +311,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
assistant["assistant_id"], # graph_id
input=None,
stream_mode="updates",
config={"configurable": {"checkpoint_id": new_state['configurable']['checkpoint_id']}}
checkpoint_id=config['checkpoint_id']
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
@@ -309,7 +326,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
{
input: null,
streamMode: "updates",
config: {"configurable": {"checkpoint_id": newState['configurable']['checkpoint_id']}},
checkpointId: config['checkpoint_id'],
}
);
for await (const chunk of streamResponse) {
@@ -323,33 +340,37 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
```bash
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
jq -r '.config.configurable.checkpoint_id' | \
sh -c '
CHECKPOINT_ID="$1"
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header "Content-Type: application/json" \
--data "{\"assistant_id\": \"agent\", \"config\": {\"configurable\": {\"checkpoint_id\": \"$CHECKPOINT_ID\"}}, \"stream_mode\": [\"updates\"]}" | \
sed "s/\r$//" | \
awk "
/^event:/ {
if (data_content != \"\" && event_type != \"metadata\") {
print data_content \"\n\"
}
sub(/^event: /, \"\", \$0)
event_type = \$0
data_content = \"\"
}
/^data:/ {
sub(/^data: /, \"\", \$0)
data_content = \$0
}
END {
if (data_content != \"\" && event_type != \"metadata\") {
print data_content \"\n\"
}
}"
' _
jq -c '.checkpoint_id' | \
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"checkpoint_id\": \"$1\",
\"stream_mode\": [
\"updates\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
sub(/^event: /, "", $0)
event_type = $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
}
'
```
Output:
@@ -60,7 +60,7 @@ Now we can start our two runs and join the second on euntil it has completed:
thread["thread_id"],
assistant_id,
input={"messages": [{"role": "human", "content": "what's the weather in nyc?"}]},
multitask_strategychrom="interrupt",
multitask_strategy="interrupt",
)
# wait until the second run completes
await client.runs.join(thread["thread_id"], run["run_id"])
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -1,5 +1,6 @@
# How to stream events
This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently.
This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently. Read more about events in this [conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#astream_events-for-streaming-tokens-of-llm-calls).
=== "Python"
+17 -13
View File
@@ -1,15 +1,7 @@
# How to stream messages from your graph
LangGraph Cloud supports multiple streaming modes. The main ones are:
This guide covers how to stream messages from your graph. In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages.
- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.
- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.
- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications.
This guide covers `stream_mode="messages"`.
In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages.
E.g., the state should look something like:
=== "Python"
@@ -23,16 +15,28 @@ E.g., the state should look something like:
messages: Annotated[list[AnyMessage], add_messages]
```
=== "Javascript"
Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above).
```js
import { type BaseMessage } from "@langchain/core/messages";
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
> [!NOTE]
> LangGraph Cloud only supports hosting graphs written in Python at the moment.
export const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
```
Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above). Or in Javascript: `import { MessagesAnnotation } from "@langchain/langgraph";`.
With `stream_mode="messages"` two things will be streamed back:
- It outputs messages produced by any chat model called inside (unless tagged in a special way)
- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like
- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like)
Read more about how the `messages` streaming mode works [here](https://langchain-ai.github.io/langgraph/cloud/concepts/api/#modemessages)
First let's set up our client and thread:
+1 -8
View File
@@ -1,13 +1,6 @@
# How to stream state updates of your graph
LangGraph Cloud supports multiple streaming modes. The main ones are:
- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.
- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.
- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications.
This guide covers `stream_mode="updates"`.
This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.```
First let's set up our client and thread:
+1 -8
View File
@@ -1,13 +1,6 @@
# How to stream full state of your graph
LangGraph Cloud supports multiple streaming modes. The main ones are:
- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.
- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.
- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications.
This guide covers `stream_mode="values"`.
This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.```
First let's set up our client and thread:
+17
View File
@@ -3,3 +3,20 @@
The LangGraph Cloud API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`).
Click <a href="/langgraph/cloud/reference/api/api_ref.html" target="_blank">here</a> to view the API reference.
## Authentication
For deployments to LangGraph Cloud, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Cloud API. The value of the header should be set to a valid LangSmith API key for the organization where the API is deployed.
Example `curl` command:
```shell
curl --request POST \
--url http://localhost:8124/assistants/search \
--header 'Content-Type: application/json' \
--header 'X-Api-Key: LANGSMITH_API_KEY' \
--data '{
"metadata": {},
"limit": 10,
"offset": 0
}'
```
+203 -4
View File
@@ -401,7 +401,17 @@ def node_a(state, config):
...
```
See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration
See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration.
### Recursion Limit
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
```python
graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}})
```
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
## Breakpoints
@@ -427,13 +437,202 @@ It's often nice to be able to visualize graphs, especially as they get more comp
## Streaming
LangGraph is built with first class support for streaming. There are several different streaming modes that LangGraph supports:
LangGraph is built with first class support for streaming. There are several different ways to stream back results
### `.stream` and `.astream`
`.stream` and `.astream` are sync and async methods for streaming back results.
There are several different modes you can specify when calling these methods (e.g. `graph.stream(..., mode="...")):
- [`"values"`](../how-tos/stream-values.ipynb): This streams the full value of the state after each step of the graph.
- [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
- [`"updates"`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
The below visualization shows the difference between the `values` and `updates` modes:
![values vs updates](../static/values_vs_updates.png)
### `.astream_events` (for streaming tokens of LLM calls)
In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
This is a standard method on all [LangChain objects](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`.
All events have (among other things) `event`, `name`, and `data` fields. What do these mean?
- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events).
- `name`: This is the name of event.
- `data`: This is the data associated with the event.
What types of things cause events to be emitted?
* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field
* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field
* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events
Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`.
To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI(model="gpt-3.5-turbo")
def call_model(state: MessagesState):
response = model.invoke(state['messages'])
return {"messages": response}
workflow = StateGraph(MessagesState)
workflow.add_node(call_model)
workflow.add_edge(START, "call_model")
workflow.add_edge("call_model", END)
app = workflow.compile()
inputs = [{"role": "user", "content": "hi!"}]
async for event in app.astream_events({"messages": inputs}, version="v2"):
kind = event["event"]
print(f"{kind}: {event['name']}")
```
```shell
on_chain_start: LangGraph
on_chain_start: __start__
on_chain_end: __start__
on_chain_start: call_model
on_chat_model_start: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_end: ChatOpenAI
on_chain_start: ChannelWrite<call_model,messages>
on_chain_end: ChannelWrite<call_model,messages>
on_chain_stream: call_model
on_chain_end: call_model
on_chain_stream: LangGraph
on_chain_end: LangGraph
```
We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input).
We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`),
stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there,
we write the results back to the channel (`ChannelWrite<call_model,messages>`) and then finish the `call_model` node and then the graph as a whole.
This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain?
Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event
since it is needed for streaming tokens from an LLM response.
These events look like:
```shell
{'event': 'on_chat_model_stream',
'name': 'ChatOpenAI',
'run_id': '3fdbf494-acce-402e-9b50-4eab46403859',
'tags': ['seq:step:1'],
'metadata': {'langgraph_step': 1,
'langgraph_node': 'call_model',
'langgraph_triggers': ['start:call_model'],
'langgraph_task_idx': 0,
'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c',
'checkpoint_ns': 'call_model',
'ls_provider': 'openai',
'ls_model_name': 'gpt-3.5-turbo',
'ls_model_type': 'chat',
'ls_temperature': 0.7},
'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')},
'parent_ids': []}
```
We can see that we have the event type and name (which we knew from before).
We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information
which tells us which node this model was invoked inside of.
Finally, `data` is a really important field. This contains the actual data for this event! Which in this case
is an AIMessageChunk. This contains the `content` for the message, as well as an `id`.
This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps
us track which chunks are part of the same message (so we can show them together in the UI).
This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a
guide for that [here](../how-tos/streaming-tokens.ipynb).
!!! warning "ASYNC IN PYTHON<=3.10"
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb).
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb).
#### Only stream tokens from specific nodes/LLMs
There are certain cases where you have multiple nodes in your graph that make LLM calls, and you do not wish to stream the tokens from every single LLM call. For example, you may use one LLM as a planner for the next steps to take, and another LLM somewhere else in the graph that actually responds to the user. In that case, you most likely WON'T want to stream tokens from the planner LLM but WILL want to stream them from the respond to user LLM. Below we show two different ways of doing this, one by streaming from specific nodes only and the second by streaming from specific LLMs only.
First, let's define our graph:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
model_1 = ChatOpenAI(model="gpt-3.5-turbo", name="model_1")
model_2 = ChatOpenAI(model="gpt-3.5-turbo", name="model_2")
def call_first_model(state: MessagesState):
response = model_1.invoke(state['messages'])
return {"messages": response}
def call_second_model(state: MessagesState):
response = model_2.invoke(state['messages'])
return {"messages": response}
workflow = StateGraph(MessagesState)
workflow.add_node(call_first_model)
workflow.add_node(call_second_model)
workflow.add_edge(START, "call_first_model")
workflow.add_edge("call_first_model", "call_second_model")
workflow.add_edge("call_second_model", END)
app = workflow.compile()
```
**Streaming from specific node**
In the case that we only want the output from a single node, we can use the event metadata to filter node names:
```python
inputs = [{"role": "user", "content": "hi!"}]
async for event in app.astream_events({"messages": inputs}, version="v2"):
# Get chat model tokens from a particular node
if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == "call_second_model":
print(event["data"]["chunk"].content, end="|", flush=True)
```
```shell
|Hello|!| How| can| I| help| you| today|?||
```
As we can see only the response from the second LLM was streamed (you can tell because we only received a single response, if we had streamed both we would have received two "Hello! How can I help you today?" messages).
**Streaming from specific LLM**
Sometimes you might want to stream from specific LLMs instead of specific nodes. This could be the case if you have multiple LLM calls inside a single node, and only want to stream the output of a specific one or if you use the same LLM in different nodes and want to stream it's output anytime it is called. We can do this by using the `name` parameter for LLMs and events:
```python
inputs = [{"role": "user", "content": "hi!"}]
async for event in app.astream_events({"messages": inputs}, version="v2"):
# Get chat model tokens from a particular LLM inside a particular node
if event["event"] == "on_chat_model_stream" and event['name'] == "model_2":
print(event["data"]["chunk"].content, end="|", flush=True)
```
```shell
|Hello|!| How| can| I| assist| you| today|?||
```
As expected, we only see a single LLM response since the response from `model_1` was not streamed.
+3 -1
View File
@@ -15,7 +15,7 @@ These how-to guides show how to achieve that controllability.
- [How to create subgraphs](subgraph.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to control graph recursion limit](recursion-limit.ipynb)
## Persistence
@@ -35,6 +35,7 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea
These guides cover common examples of that.
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb)
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb)
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb)
- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb)
@@ -78,6 +79,7 @@ These guides show how to use different streaming modes.
- [How to use a Pydantic model as your state](state-model.ipynb)
- [How to use a context object in state](state-context-key.ipynb)
- [How to add node retries](node-retries.ipynb)
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
## Prebuilt ReAct Agent
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+4 -1
View File
@@ -129,6 +129,7 @@ nav:
- Create subgraphs: how-tos/subgraph.ipynb
- Create branches for parallel execution: how-tos/branching.ipynb
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
- Control graph recursion limit: how-tos/recursion-limit.ipynb
- Persistence:
- Add persistence ("memory"): how-tos/persistence.ipynb
- Manage conversation history: how-tos/memory/manage-conversation-history.ipynb
@@ -139,6 +140,7 @@ nav:
- Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb
- Human-in-the-loop:
- Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb
- Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
- Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb
- View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb
- Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb
@@ -169,11 +171,12 @@ nav:
- Visualize your graph: how-tos/visualization.ipynb
- Add runtime configuration: how-tos/configuration.ipynb
- Add node retries: how-tos/node-retries.ipynb
- How to return structured output from a ReAct agent: how-tos/react-agent-structured-output.ipynb
- Prebuilt ReAct Agent:
- Create a ReAct agent: how-tos/create-react-agent.ipynb
- Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb
- Add a system prompt to a ReAct agent: how-tos/create-react-agent-system-prompt.ipynb
- Add human-in-the-Loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb
- Add Human-in-the-loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb
- "Conceptual Guides":
- "concepts/index.md"
- LangGraph for Agentic Applications: concepts/high_level.md
@@ -235,7 +235,7 @@
" # Call the chat bot\n",
" chat_bot_response = my_chat_bot(messages)\n",
" # Respond with an AI Message\n",
" return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}"
" return {\"messages\": [AIMessage(content=chat_bot_response[\"content\"])]}"
]
},
{
@@ -270,7 +270,7 @@
" # Call the simulated user\n",
" response = simulated_user.invoke({\"messages\": new_messages})\n",
" # This response is an AI message - we need to flip this to be a human message\n",
" return {\"messages\":[HumanMessage(content=response.content)]}"
" return {\"messages\": [HumanMessage(content=response.content)]}"
]
},
{
@@ -331,6 +331,7 @@
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"graph_builder = StateGraph(State)\n",
"graph_builder.add_node(\"user\", simulated_user_node)\n",
"graph_builder.add_node(\"chat_bot\", chat_bot_node)\n",
@@ -398,14 +399,6 @@
" print(chunk)\n",
" print(\"----\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -359,14 +359,6 @@
" evaluation=evaluation,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26735ed2-766d-4e0a-a185-b2295a0615b8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -79,7 +79,7 @@
"\n",
"\n",
"def info_chain(state):\n",
" messages = get_messages_info(state['messages'])\n",
" messages = get_messages_info(state[\"messages\"])\n",
" response = llm_with_tool.invoke(messages)\n",
" return {\"messages\": [response]}"
]
@@ -126,7 +126,7 @@
"\n",
"\n",
"def prompt_gen_chain(state):\n",
" messages = get_prompt_messages(state['messages'])\n",
" messages = get_prompt_messages(state[\"messages\"])\n",
" response = llm.invoke(messages)\n",
" return {\"messages\": [response]}"
]
@@ -158,7 +158,7 @@
"\n",
"\n",
"def get_state(state) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n",
" messages = state['messages']\n",
" messages = state[\"messages\"]\n",
" if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:\n",
" return \"add_tool_message\"\n",
" elif not isinstance(messages[-1], HumanMessage):\n",
@@ -190,9 +190,11 @@
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"memory = MemorySaver()\n",
"workflow = StateGraph(State)\n",
"workflow.add_node(\"info\", info_chain)\n",
@@ -201,9 +203,14 @@
"\n",
"@workflow.add_node\n",
"def add_tool_message(state: State):\n",
" return {\"messages\": [ToolMessage(\n",
" content=\"Prompt generated!\", tool_call_id=state['messages'][-1].tool_calls[0][\"id\"]\n",
" )]}\n",
" return {\n",
" \"messages\": [\n",
" ToolMessage(\n",
" content=\"Prompt generated!\",\n",
" tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n",
" )\n",
" ]\n",
" }\n",
"\n",
"\n",
"workflow.add_conditional_edges(\"info\", get_state)\n",
@@ -364,7 +371,7 @@
" for output in graph.stream(\n",
" {\"messages\": [HumanMessage(content=user)]}, config=config, stream_mode=\"updates\"\n",
" ):\n",
" last_message = next(iter(output.values()))['messages'][-1]\n",
" last_message = next(iter(output.values()))[\"messages\"][-1]\n",
" last_message.pretty_print()\n",
"\n",
" if output and \"prompt\" in output:\n",
@@ -172,14 +172,6 @@
"):\n",
" print(event)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "666d78f1-019a-433e-839e-52d2ebb3d9c8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -166,14 +166,6 @@
"):\n",
" print(event)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c26df68-c447-4a88-bc94-59df42b117b5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+124 -36
View File
@@ -24,15 +24,43 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 1,
"id": "816523d0-0b59-47cf-9f4c-4838024efe22",
"metadata": {},
"outputs": [],
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.messages import BaseMessage, HumanMessage\n\nfrom langgraph.graph import END, StateGraph, START\n\nmodel = ChatAnthropic(model_name=\"claude-2.1\")\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n\n\ndef _call_model(state):\n response = model.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"model = ChatAnthropic(model_name=\"claude-2.1\")\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]\n",
"\n",
"\n",
"def _call_model(state):\n",
" response = model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 2,
"id": "070f11a6-2441-4db5-9df6-e318f110e281",
"metadata": {},
"outputs": [
@@ -40,15 +68,17 @@
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='hi'),\n",
" AIMessage(content='Hello!', response_metadata={'id': 'msg_01YZj7CVCUSc76faX4VM9i5d', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-d343db34-598c-46a2-93d6-ffa886d9b264-0')]}"
" AIMessage(content='Hello!', response_metadata={'id': 'msg_012SakNGNitBcKJgc9yZ1Asv', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-9e375cd7-ae84-4db2-981c-c7e18ecabddf-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}"
]
},
"execution_count": 8,
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
},
{
"cell_type": "markdown",
@@ -58,17 +88,44 @@
"## Configure the graph\n",
"\n",
"Great! Now let's suppose that we want to extend this example so the user is able to choose from multiple llms.\n",
"We can easily do that by passing in a config.\n",
"We can easily do that by passing in a config. Any configuration information needs to be passed inside `configurable` key as shown below.\n",
"This config is meant to contain things are not part of the input (and therefore that we don't want to track as part of the state)."
]
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 6,
"id": "c01f1e7c-8e8b-4e26-98f7-56ac225077b4",
"metadata": {},
"outputs": [],
"source": ["from langchain_openai import ChatOpenAI\n\nopenai_model = ChatOpenAI()\n\nmodels = {\n \"anthropic\": model,\n \"openai\": openai_model,\n}\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n response = m.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
"source": [
"from langchain_openai import ChatOpenAI\n",
"from typing import Optional\n",
"from langchain_core.runnables.config import RunnableConfig\n",
"\n",
"openai_model = ChatOpenAI()\n",
"\n",
"models = {\n",
" \"anthropic\": model,\n",
" \"openai\": openai_model,\n",
"}\n",
"\n",
"def _call_model(state: AgentState, config: RunnableConfig):\n",
" # Access the config through the configurable key\n",
" model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n",
" model = models[model_name]\n",
" response = model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
},
{
"cell_type": "markdown",
@@ -80,7 +137,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 7,
"id": "ef50f048-fc43-40c0-b713-346408fcf052",
"metadata": {},
"outputs": [
@@ -88,15 +145,17 @@
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='hi'),\n",
" AIMessage(content='Hello!', response_metadata={'id': 'msg_01EedReFyXmonWXPKhYre7Jb', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-1c6feaa0-bd6f-433a-8264-209d72c85db7-0')]}"
" AIMessage(content='Hello!', response_metadata={'id': 'msg_0133PAX5DyoUYL1gZiGR8NXs', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-03e8bd8b-fa09-4258-920d-8f53a7b91fcc-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}"
]
},
"execution_count": 12,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
},
{
"cell_type": "markdown",
@@ -108,7 +167,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 8,
"id": "f2f7c74b-9fb0-41c6-9728-dcf9d8a3c397",
"metadata": {},
"outputs": [
@@ -116,15 +175,18 @@
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='hi'),\n",
" AIMessage(content='Hello! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'stop', 'logprobs': None}, id='run-d41ffb62-e164-45a1-862c-d288c6ad100a-0')]}"
" AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-6d0c7c25-03de-49d6-b3be-ff0858d17122-0', usage_metadata={'input_tokens': 8, 'output_tokens': 9, 'total_tokens': 17})]}"
]
},
"execution_count": 13,
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": ["config = {\"configurable\": {\"model\": \"openai\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"]
"source": [
"config = {\"configurable\": {\"model\": \"openai\"}}\n",
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"
]
},
{
"cell_type": "markdown",
@@ -136,15 +198,44 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": 9,
"id": "f0393a43-9fbe-4056-972f-3e91ea329041",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import SystemMessage\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n messages = state[\"messages\"]\n if \"system_message\" in config[\"configurable\"]:\n messages = [\n SystemMessage(content=config[\"configurable\"][\"system_message\"])\n ] + messages\n response = m.invoke(messages)\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
"source": [
"from langchain_core.messages import SystemMessage\n",
"\n",
"# We can define a config schema to specify the configuration options for the graph\n",
"# A config schema is useful for indicating which fields are available in the configurable dict inside the config\n",
"class ConfigSchema(TypedDict):\n",
" model: Optional[str]\n",
" system_message: Optional[str]\n",
"\n",
"def _call_model(state: AgentState, config: RunnableConfig):\n",
" # Access the config through the configurable key\n",
" model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n",
" model = models[model_name]\n",
" messages = state[\"messages\"]\n",
" if \"system_message\" in config[\"configurable\"]:\n",
" messages = [\n",
" SystemMessage(content=config[\"configurable\"][\"system_message\"])\n",
" ] + messages\n",
" response = model.invoke(messages)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph - note that we pass in the configuration schema here, but it is not necessary\n",
"workflow = StateGraph(AgentState, ConfigSchema)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
},
{
"cell_type": "code",
"execution_count": 19,
"execution_count": 10,
"id": "718685f7-4cdd-4181-9fc8-e7762d584727",
"metadata": {},
"outputs": [
@@ -152,19 +243,21 @@
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='hi'),\n",
" AIMessage(content='Hello!', response_metadata={'id': 'msg_01Ts56eVLSrUbzVMbzLnXc3M', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-f75a4389-b72e-4d47-8f3e-bedc6a060f66-0')]}"
" AIMessage(content='Hello!', response_metadata={'id': 'msg_01TVJvxCXsCT9JVe7A4iUUi9', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-627eb685-c4d7-481d-9095-c0a1822e8c10-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}"
]
},
"execution_count": 19,
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": 11,
"id": "e043a719-f197-46ef-9d45-84740a39aeb0",
"metadata": {},
"outputs": [
@@ -172,23 +265,18 @@
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='hi'),\n",
" AIMessage(content='Ciao!', response_metadata={'id': 'msg_01RzFCii8WhbbkFm16nUquxk', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-9492f0e4-f223-41c2-81a6-6f0cb6a14fe6-0')]}"
" AIMessage(content='Ciao!', response_metadata={'id': 'msg_01CpBD1cMCYvvPX2cogUawJj', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-6ef2fea6-9bfa-4266-bd05-263160a1db7b-0', usage_metadata={'input_tokens': 14, 'output_tokens': 7, 'total_tokens': 21})]}"
]
},
"execution_count": 20,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": ["config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a5c5f7f4-4b0e-4cde-93a6-c1c6329b8591",
"metadata": {},
"outputs": [],
"source": [""]
"source": [
"config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\n",
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"
]
}
],
"metadata": {
@@ -207,7 +295,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
+107 -20
View File
@@ -73,7 +73,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 1,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
@@ -92,14 +92,14 @@
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
"def get_weather(location: str):\n",
" \"\"\"Use this to get weather information from a given location.\"\"\"\n",
" if location.lower() in [\"nyc\", \"new york\"]:\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" elif location.lower() in [\"sf\", \"san francisco\"]:\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
" raise AssertionError(\"Unknown Location\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
@@ -144,7 +144,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 3,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [
@@ -154,26 +154,35 @@
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in SF?\n",
"what is the weather in SF?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_0OMmuTLec9t8kxMVkllZCSxo)\n",
" Call ID: call_0OMmuTLec9t8kxMVkllZCSxo\n",
" get_weather (call_TcDfLuoCKLmQ7eG71SedxLZ6)\n",
" Call ID: call_TcDfLuoCKLmQ7eG71SedxLZ6\n",
" Args:\n",
" city: sf\n"
" location: San Francisco, CA\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"config = {\"configurable\": {\"thread_id\": \"42\"}}\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in SF?\")]}\n",
"inputs = {\"messages\": [(\"user\", \"what is the weather in SF?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, config, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "ca40a719",
"metadata": {},
"source": [
"We can verify that our graph stopped at the right place:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 4,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [
@@ -190,9 +199,87 @@
"print(\"Next step: \", snapshot.next)"
]
},
{
"cell_type": "markdown",
"id": "7de6ca78",
"metadata": {},
"source": [
"Now we can either approve or edit the tool call before proceeding to the next node. If we wanted to approve the tool call, we would simply continue streaming the graph with `None` input. If we wanted to edit the tool call we need to update the state to have the correct tool call, and then after the update has been applied we can continue.\n",
"\n",
"We can try resuming and we will see an error arise:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "740bbaeb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"Error: AssertionError('Unknown Location')\n",
" Please fix your mistakes.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"It seems there was an issue with the location provided. Let's try specifying \"San Francisco, California\" more clearly.\n",
"Tool Calls:\n",
" get_weather (call_TZm9HCShGNEreglVJcmUdXqG)\n",
" Call ID: call_TZm9HCShGNEreglVJcmUdXqG\n",
" Args:\n",
" location: San Francisco, California\n"
]
}
],
"source": [
"print_stream(graph.stream(None, config, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "c1cf5950",
"metadata": {},
"source": [
"This error arose because our tool argument of \"San Francisco, CA\" is not a location our tool recognizes.\n",
"\n",
"Let's show how we would edit the tool call to search for \"San Francisco\" instead of \"San Francisco, CA\" - since our tool as written treats \"San Francisco, CA\" as an unknown location. We will update the state and then resume streaming the graph and should see no errors arise:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "1c81ed9f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '42',\n",
" 'checkpoint_ns': '',\n",
" 'checkpoint_id': '1ef66368-9772-67ea-8004-07c779869a0a'}}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"state = graph.get_state(config)\n",
"\n",
"last_message = state.values['messages'][-1]\n",
"last_message.tool_calls[0]['args'] = {\"location\": \"San Francisco\"}\n",
"\n",
"graph.update_state(config, {\"messages\": [ last_message]})"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "83148e08-63e8-49e5-a08b-02dc907bed1d",
"metadata": {},
"outputs": [
@@ -206,7 +293,7 @@
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny.\n"
"The weather in San Francisco is currently sunny. Enjoy the sunshine!\n"
]
}
],
@@ -215,12 +302,12 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6f6f8965-b016-4e25-be63-31c00fc0a6de",
"cell_type": "markdown",
"id": "8202a5f9",
"metadata": {},
"outputs": [],
"source": []
"source": [
"Fantastic! Our graph updated properly to query the weather in San Francisco and got the correct \"It's always sunny in sf\" response from the tool, and then responded to the user accordingly."
]
}
],
"metadata": {
@@ -239,7 +326,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
-8
View File
@@ -221,14 +221,6 @@
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
"print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -173,14 +173,6 @@
"\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -99,46 +99,50 @@
" # Backup - we will use this to \"reset\" our DB in each section\n",
" shutil.copy(local_file, backup_file)\n",
"# Convert the flights to present time for our tutorial\n",
"conn = sqlite3.connect(local_file)\n",
"cursor = conn.cursor()\n",
"def update_dates(file):\n",
" shutil.copy(backup_file, file)\n",
" conn = sqlite3.connect(file)\n",
" cursor = conn.cursor()\n",
"\n",
"tables = pd.read_sql(\n",
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
").name.tolist()\n",
"tdf = {}\n",
"for t in tables:\n",
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
" tables = pd.read_sql(\n",
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
" ).name.tolist()\n",
" tdf = {}\n",
" for t in tables:\n",
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
"\n",
"example_time = pd.to_datetime(\n",
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
").max()\n",
"current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
"time_diff = current_time - example_time\n",
" example_time = pd.to_datetime(\n",
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
" ).max()\n",
" current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
" time_diff = current_time - example_time\n",
"\n",
"tdf[\"bookings\"][\"book_date\"] = (\n",
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
" + time_diff\n",
")\n",
"\n",
"datetime_columns = [\n",
" \"scheduled_departure\",\n",
" \"scheduled_arrival\",\n",
" \"actual_departure\",\n",
" \"actual_arrival\",\n",
"]\n",
"for column in datetime_columns:\n",
" tdf[\"flights\"][column] = (\n",
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
" tdf[\"bookings\"][\"book_date\"] = (\n",
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
" + time_diff\n",
" )\n",
"\n",
"for table_name, df in tdf.items():\n",
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
"del df\n",
"del tdf\n",
"conn.commit()\n",
"conn.close()\n",
" datetime_columns = [\n",
" \"scheduled_departure\",\n",
" \"scheduled_arrival\",\n",
" \"actual_departure\",\n",
" \"actual_arrival\",\n",
" ]\n",
" for column in datetime_columns:\n",
" tdf[\"flights\"][column] = (\n",
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
" )\n",
"\n",
"db = local_file # We'll be using this local file as our DB in this tutorial"
" for table_name, df in tdf.items():\n",
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
" del df\n",
" del tdf\n",
" conn.commit()\n",
" conn.close()\n",
"\n",
" return file\n",
"\n",
"db = update_dates(local_file)"
]
},
{
@@ -225,7 +229,14 @@
"\n",
"Define the (`fetch_user_flight_information`) tool to let the agent see the current user's flight information. Then define tools to search for flights and manage the passenger's bookings stored in the SQL database.\n",
"\n",
"We use `ensure_config` to pass in the `passenger_id` in via configurable parameters. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information."
"We the can [access the RunnableConfig](https://python.langchain.com/v0.2/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n",
"\n",
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">Compatibility</p>\n",
" <p>\n",
" This tutorial expects `langchain-core>=0.2.16` to use the injected RunnableConfig. Prior to that, you'd use `ensure_config` to collect the config from context.\n",
" </p>\n",
"</div> \n"
]
},
{
@@ -240,18 +251,17 @@
"from typing import Optional\n",
"\n",
"import pytz\n",
"from langchain_core.runnables import ensure_config\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"\n",
"@tool\n",
"def fetch_user_flight_information() -> list[dict]:\n",
"def fetch_user_flight_information(config: RunnableConfig) -> list[dict]:\n",
" \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n",
"\n",
" Returns:\n",
" A list of dictionaries where each dictionary contains the ticket details,\n",
" associated flight details, and the seat assignments for each ticket belonging to the user.\n",
" \"\"\"\n",
" config = ensure_config() # Fetch from the context\n",
" configuration = config.get(\"configurable\", {})\n",
" passenger_id = configuration.get(\"passenger_id\", None)\n",
" if not passenger_id:\n",
@@ -328,9 +338,10 @@
"\n",
"\n",
"@tool\n",
"def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n",
"def update_ticket_to_new_flight(\n",
" ticket_no: str, new_flight_id: int, *, config: RunnableConfig\n",
") -> str:\n",
" \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n",
" config = ensure_config()\n",
" configuration = config.get(\"configurable\", {})\n",
" passenger_id = configuration.get(\"passenger_id\", None)\n",
" if not passenger_id:\n",
@@ -396,9 +407,8 @@
"\n",
"\n",
"@tool\n",
"def cancel_ticket(ticket_no: str) -> str:\n",
"def cancel_ticket(ticket_no: str, *, config: RunnableConfig) -> str:\n",
" \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n",
" config = ensure_config()\n",
" configuration = config.get(\"configurable\", {})\n",
" passenger_id = configuration.get(\"passenger_id\", None)\n",
" if not passenger_id:\n",
@@ -1744,7 +1754,7 @@
"]\n",
"\n",
"# Update with the backup file so we can restart from the original place in each section\n",
"shutil.copy(backup_file, db)\n",
"db = update_dates(db)\n",
"thread_id = str(uuid.uuid4())\n",
"\n",
"config = {\n",
@@ -2298,7 +2308,7 @@
"import uuid\n",
"\n",
"# Update with the backup file so we can restart from the original place in each section\n",
"shutil.copy(backup_file, db)\n",
"db = update_dates(db)\n",
"thread_id = str(uuid.uuid4())\n",
"\n",
"config = {\n",
@@ -2902,7 +2912,7 @@
"import uuid\n",
"\n",
"# Update with the backup file so we can restart from the original place in each section\n",
"shutil.copy(backup_file, db)\n",
"db = update_dates(db)\n",
"thread_id = str(uuid.uuid4())\n",
"\n",
"config = {\n",
@@ -4324,7 +4334,7 @@
"import uuid\n",
"\n",
"# Update with the backup file so we can restart from the original place in each section\n",
"shutil.copy(backup_file, db)\n",
"db = update_dates(db)\n",
"thread_id = str(uuid.uuid4())\n",
"\n",
"config = {\n",
@@ -4407,7 +4417,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
"version": "3.12.2"
}
},
"nbformat": 4,
File diff suppressed because one or more lines are too long
-8
View File
@@ -1010,14 +1010,6 @@
"\n",
"If you notice high retry rates (using an observability tool like LangSmith), you can set up a rule to send the failure cases to a dataset alongside the corrected values and then automatically program those into your prompts or schemas (or use them as few-shots to have semantically relevant demonstrations)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ae295b1-da58-4cc9-834b-70e1466f8695",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
File diff suppressed because one or more lines are too long
@@ -541,14 +541,6 @@
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "78780afe-409d-46cd-a734-e82538cdd8de",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -141,15 +141,18 @@
" print(\"----\")\n",
" return \"Sunny!\"\n",
"\n",
"model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools([weather_search])\n",
"\n",
"model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools(\n",
" [weather_search]\n",
")\n",
"\n",
"\n",
"class State(MessagesState):\n",
" \"\"\"Simple state.\"\"\"\n",
"\n",
"\n",
"def call_llm(state):\n",
" return {\n",
" \"messages\": [model.invoke(state['messages'])]\n",
" }\n",
" return {\"messages\": [model.invoke(state[\"messages\"])]}\n",
"\n",
"\n",
"def human_review_node(state):\n",
@@ -159,28 +162,30 @@
"def run_tool(state):\n",
" new_messages = []\n",
" tools = {\"weather_search\": weather_search}\n",
" tool_calls = state['messages'][-1].tool_calls\n",
" tool_calls = state[\"messages\"][-1].tool_calls\n",
" for tool_call in tool_calls:\n",
" tool = tools[tool_call['name']]\n",
" result = tool.invoke(tool_call['args'])\n",
" new_messages.append({\n",
" \"role\": \"tool\",\n",
" \"name\": tool_call['name'],\n",
" \"content\": result,\n",
" \"tool_call_id\": tool_call['id']\n",
" })\n",
" tool = tools[tool_call[\"name\"]]\n",
" result = tool.invoke(tool_call[\"args\"])\n",
" new_messages.append(\n",
" {\n",
" \"role\": \"tool\",\n",
" \"name\": tool_call[\"name\"],\n",
" \"content\": result,\n",
" \"tool_call_id\": tool_call[\"id\"],\n",
" }\n",
" )\n",
" return {\"messages\": new_messages}\n",
"\n",
"\n",
"def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n",
" if len(state['messages'][-1].tool_calls) == 0:\n",
" if len(state[\"messages\"][-1].tool_calls) == 0:\n",
" return END\n",
" else:\n",
" return \"human_review_node\"\n",
"\n",
"\n",
"def route_after_human(state) -> Literal[\"run_tool\", \"call_llm\"]:\n",
" if isinstance(state['messages'][-1], AIMessage):\n",
" if isinstance(state[\"messages\"][-1], AIMessage):\n",
" return \"run_tool\"\n",
" else:\n",
" return \"call_llm\"\n",
@@ -460,35 +465,35 @@
"print(\"Current State:\")\n",
"print(state.values)\n",
"print(\"\\nCurrent Tool Call ID:\")\n",
"current_content = state.values['messages'][-1].content\n",
"current_id = state.values['messages'][-1].id\n",
"tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n",
"current_content = state.values[\"messages\"][-1].content\n",
"current_id = state.values[\"messages\"][-1].id\n",
"tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n",
"print(tool_call_id)\n",
"\n",
"# We now need to construct a replacement tool call.\n",
"# We will change the argument to be `San Francisco, USA`\n",
"# Note that we could change any number of arguments or tool names - it just has to be a valid one\n",
"new_message = {\n",
" \"role\": \"assistant\", \n",
" \"role\": \"assistant\",\n",
" \"content\": current_content,\n",
" \"tool_calls\": [\n",
" {\n",
" \"id\": tool_call_id,\n",
" \"name\": \"weather_search\",\n",
" \"args\": {\"city\": \"San Francisco, USA\"}\n",
" \"args\": {\"city\": \"San Francisco, USA\"},\n",
" }\n",
" ],\n",
" # This is important - this needs to be the same as the message you replacing!\n",
" # Otherwise, it will show up as a separate message\n",
" \"id\": current_id\n",
" \"id\": current_id,\n",
"}\n",
"graph.update_state(\n",
" # This is the config which represents this thread\n",
" thread, \n",
" thread,\n",
" # This is the updated value we want to push\n",
" {\"messages\": [new_message]}, \n",
" {\"messages\": [new_message]},\n",
" # We push this update acting as our human_review_node\n",
" as_node=\"human_review_node\"\n",
" as_node=\"human_review_node\",\n",
")\n",
"\n",
"# Let's now continue executing from here\n",
@@ -595,26 +600,26 @@
"print(\"Current State:\")\n",
"print(state.values)\n",
"print(\"\\nCurrent Tool Call ID:\")\n",
"tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n",
"tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n",
"print(tool_call_id)\n",
"\n",
"# We now need to construct a replacement tool call.\n",
"# We will change the argument to be `San Francisco, USA`\n",
"# Note that we could change any number of arguments or tool names - it just has to be a valid one\n",
"new_message = {\n",
" \"role\": \"tool\", \n",
" \"role\": \"tool\",\n",
" # This is our natural language feedback\n",
" \"content\": \"User requested changes: pass in the country as well\",\n",
" \"name\": \"weather_search\",\n",
" \"tool_call_id\": tool_call_id\n",
" \"tool_call_id\": tool_call_id,\n",
"}\n",
"graph.update_state(\n",
" # This is the config which represents this thread\n",
" thread, \n",
" thread,\n",
" # This is the updated value we want to push\n",
" {\"messages\": [new_message]}, \n",
" {\"messages\": [new_message]},\n",
" # We push this update acting as our human_review_node\n",
" as_node=\"human_review_node\"\n",
" as_node=\"human_review_node\",\n",
")\n",
"\n",
"# Let's now continue executing from here\n",
@@ -55,7 +55,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
@@ -227,7 +227,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"Tell me how you want to update the state: go to step 3!\n"
@@ -636,14 +636,6 @@
"for event in app.stream(None, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+4 -8
View File
@@ -33,15 +33,19 @@
"from langgraph.graph import StateGraph, START, END\n",
"from typing import TypedDict\n",
"\n",
"\n",
"class InputState(TypedDict):\n",
" question: str\n",
"\n",
"\n",
"class OutputState(TypedDict):\n",
" answer: str\n",
"\n",
"\n",
"def answer_node(state: InputState):\n",
" return {\"answer\": \"bye\"}\n",
"\n",
"\n",
"graph = StateGraph(input=InputState, output=OutputState)\n",
"graph.add_node(answer_node)\n",
"graph.add_edge(START, \"answer_node\")\n",
@@ -58,14 +62,6 @@
"source": [
"Notice that the output of invoke only includes the output schema."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b952a554-f2a4-4be3-81ab-2e08f0f441c2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+102 -78
View File
@@ -38,7 +38,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"id": "abbd6948-e9a3-47ca-89c7-7ac2fc5eca8b",
"metadata": {},
"outputs": [],
@@ -73,7 +73,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 47,
"id": "e7476bb2-1a51-42f6-b7ae-82a0300bbf84",
"metadata": {},
"outputs": [],
@@ -147,7 +147,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 78,
"id": "15dd9639-691f-4906-9012-83fd6e9ac126",
"metadata": {},
"outputs": [
@@ -211,7 +211,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 79,
"id": "45689d40-d8df-4316-a121-6ea9c87d2efe",
"metadata": {},
"outputs": [],
@@ -270,7 +270,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 80,
"id": "bbdcb57b-5362-4b9e-88db-fb3fae443fb0",
"metadata": {},
"outputs": [],
@@ -282,7 +282,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 81,
"id": "730490c6-6e3a-4173-82a1-9eb9d5eeff20",
"metadata": {},
"outputs": [
@@ -292,7 +292,7 @@
"text": [
"description='tavily_search_results_json(query=\"the search query\") - a search engine.' max_results=1 {'query': 'current temperature in San Francisco'}\n",
"---\n",
"name='math' description='math(problem: str, context: Optional[List[str]] = None, config: Optional[langchain_core.runnables.config.RunnableConfig] = None) - math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema=<class 'pydantic.v1.main.mathSchema'> func=<function get_math_tool.<locals>.calculate_expression at 0x10f354ea0> {'problem': 'raise $0 to the 3rd power', 'context': ['$0']}\n",
"name='math' description='math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema=<class 'pydantic.v1.main.mathSchema'> func=<function get_math_tool.<locals>.calculate_expression at 0x14e1049a0> {'problem': 'x^3', 'context': ['$1']}\n",
"---\n",
"join ()\n",
"---\n"
@@ -331,7 +331,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 82,
"id": "c1fbafdd-42d4-4575-8466-e5951cee71f4",
"metadata": {
"jp-MarkdownHeadingCollapsed": true
@@ -493,7 +493,7 @@
" }\n",
" tool_messages = [\n",
" FunctionMessage(\n",
" name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n",
" name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}, tool_call_id = k\n",
" )\n",
" for k, (name, task_args, obs) in new_observations.items()\n",
" ]\n",
@@ -502,7 +502,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 83,
"id": "052f6b16-103a-40e9-94dd-8fcc37e77ba4",
"metadata": {},
"outputs": [],
@@ -526,7 +526,7 @@
" \"tasks\": tasks,\n",
" }\n",
" )\n",
" return {\"messages\":[scheduled_tasks]}"
" return {\"messages\": scheduled_tasks}"
]
},
{
@@ -541,29 +541,29 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 84,
"id": "55142257-2674-4a47-988e-0d2810917329",
"metadata": {},
"outputs": [],
"source": [
"tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])"
"tool_messages = plan_and_schedule.invoke({\"messages\":[HumanMessage(content=example_question)]})['messages']"
]
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 85,
"id": "a98e0525-2fcf-4fa1-baf6-79858bb8a6bd",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[FunctionMessage(content='[]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'),\n",
" FunctionMessage(content='ValueError(\\'Failed to evaluate \"N/A\". Raised error: KeyError(\\\\\\'A\\\\\\'). Please try again with a valid numerical expression\\')', additional_kwargs={'idx': 1}, name='math'),\n",
" FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]"
"[FunctionMessage(content=\"[{'url': 'https://www.wunderground.com/weather/us/ca/san-francisco', 'content': 'Current Weather for Popular Cities . San Francisco, CA 82 ° F Sunny; Manhattan, NY warning 84 ° F Sunny; Schiller Park, IL (60176) warning 97 ° F Mostly Cloudy; Boston, MA warning 74 ° F ...'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in San Francisco'}}, name='tavily_search_results_json', tool_call_id=1),\n",
" FunctionMessage(content='551368', additional_kwargs={'idx': 2, 'args': {'problem': 'x ** 3', 'context': ['$1']}}, name='math', tool_call_id=2),\n",
" FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]"
]
},
"execution_count": 12,
"execution_count": 85,
"metadata": {},
"output_type": "execute_result"
}
@@ -589,12 +589,11 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 86,
"id": "942dab42-ad42-4ba2-90d5-49edbe4fae68",
"metadata": {},
"outputs": [],
"source": [
"from langchain.chains.openai_functions import create_structured_output_runnable\n",
"from langchain_core.messages import AIMessage\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
@@ -625,7 +624,7 @@
") # You can optionally add examples\n",
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
"\n",
"runnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)"
"runnable = joiner_prompt | llm.with_structured_output(JoinOutputs)"
]
},
{
@@ -639,7 +638,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 87,
"id": "951a33cf-2a05-4a33-899a-0ab1d97122fa",
"metadata": {},
"outputs": [],
@@ -647,13 +646,14 @@
"def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n",
" response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n",
" if isinstance(decision.action, Replan):\n",
" return response + [\n",
" return {\"messages\": response + [\n",
" SystemMessage(\n",
" content=f\"Context from last attempt: {decision.action.feedback}\"\n",
" )\n",
" ]\n",
" }\n",
" else:\n",
" return {\"messages\":response + [AIMessage(content=decision.action.response)]}\n",
" return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n",
"\n",
"\n",
"def select_recent_messages(state) -> dict:\n",
@@ -671,7 +671,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 88,
"id": "1e49d4b1-8266-4520-a566-1448b1c31c8f",
"metadata": {},
"outputs": [],
@@ -681,24 +681,24 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 89,
"id": "31854dfd-b82f-4c24-9b58-6bae66777909",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[AIMessage(content='Thought: The search did not return any results, and the attempt to calculate the temperature in San Francisco raised to the 3rd power failed due to missing temperature information.'),\n",
" SystemMessage(content='Context from last attempt: I need to find the current temperature in San Francisco before calculating its value raised to the 3rd power.')]"
"{'messages': [AIMessage(content=\"Thought: We have the current temperature in San Francisco (82 °F) and have calculated the temperature raised to the 3rd power (551368). Therefore, we can provide an answer to the user's question.\"),\n",
" AIMessage(content='The temperature in San Francisco raised to the 3rd power is 551368.')]}"
]
},
"execution_count": 16,
"execution_count": 89,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"joiner.invoke(input_messages)"
"joiner.invoke({\"messages\":input_messages})"
]
},
{
@@ -717,7 +717,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 90,
"id": "768b5f11-e3d2-47be-8143-a7dcd8765243",
"metadata": {},
"outputs": [],
@@ -726,9 +726,11 @@
"from langgraph.graph.message import add_messages\n",
"from typing import Annotated\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"graph_builder = StateGraph(State)\n",
"\n",
"# 1. Define vertices\n",
@@ -752,9 +754,9 @@
"\n",
"\n",
"graph_builder.add_conditional_edges(\n",
" start_key=\"join\",\n",
" \"join\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" condition=should_continue,\n",
" should_continue,\n",
")\n",
"graph_builder.add_edge(START, \"plan_and_schedule\")\n",
"chain = graph_builder.compile()"
@@ -772,7 +774,7 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": 91,
"id": "5bc4584a-e31c-4065-805e-76a6db30676a",
"metadata": {},
"outputs": [
@@ -780,28 +782,24 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json')]}\n",
"{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.investopedia.com/articles/investing/011516/new-yorks-economy-6-industries-driving-gdp-growth.asp', 'content': 'The manufacturing sector is a leader in railroad rolling stock, as many of the earliest railroads were financed or founded in New York; garments, as New York City is the fashion capital of the U.S.; elevator parts; glass; and many other products.\\\\n Educational Services\\\\nThough not typically thought of as a leading industry, the educational sector in New York nonetheless has a substantial impact on the state and its residents, and in attracting new talent that eventually enters the New York business scene. New York has seen a large uptick in college attendees, both young and old, over the 21st century, and an increasing number of new employees in other New York sectors were educated in the state. New York City is the leading job hub for banking, finance, and communication in the U.S. New York is also a major manufacturing center and shipping port, and it has a thriving technological sector.\\\\n The state of New York has the third-largest economy in the United States with a gross domestic product (GDP) of $1.7 trillion, trailing only Texas and California.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'GDP of New York'}}, name='tavily_search_results_json', tool_call_id=1)]}}\n",
"---\n",
"{'join': [AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget.\")]}\n",
"---\n",
"{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json')]}\n",
"---\n",
"{'join': [AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n",
"---\n",
"{'__end__': [HumanMessage(content=\"What's the GDP of New York?\"), FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget. - Begin counting at : 1\"), FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: The information required to answer the user's question has been found. The GDP of New York is mentioned as $1.7 trillion, making it the third-largest economy in the United States.\", id='d656a605-e4c4-470d-9b29-31794f298a71'), AIMessage(content='The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.', id='5135758e-d01e-4360-bb6a-31025b723d8c')]}}\n",
"---\n"
]
}
],
"source": [
"for step in chain.stream({\"messages\":[HumanMessage(content=\"What's the GDP of New York?\")]}):\n",
"for step in chain.stream(\n",
" {\"messages\": [HumanMessage(content=\"What's the GDP of New York?\")]}\n",
"):\n",
" print(step)\n",
" print(\"---\")"
]
},
{
"cell_type": "code",
"execution_count": 19,
"execution_count": 92,
"id": "b96efd08-5314-44f0-a694-3073b638adad",
"metadata": {},
"outputs": [
@@ -809,13 +807,13 @@
"name": "stdout",
"output_type": "stream",
"text": [
"The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.\n"
"The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.\n"
]
}
],
"source": [
"# Final answer\n",
"print(step[END][-1].content)"
"print(step['join']['messages'][-1].content)"
]
},
{
@@ -830,7 +828,7 @@
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": 93,
"id": "0b3a0916-d8ca-4092-b91c-d9e2b05259d8",
"metadata": {},
"outputs": [
@@ -838,26 +836,21 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the Worlds Oldest Parrot? Discover the Worlds Oldest Parrot Advertisement of debate, so well detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]}\n",
"{'plan_and_schedule': {'messages': [FunctionMessage(content='[{\\'url\\': \\'https://en.wikipedia.org/wiki/Cookie_(cockatoo)\\', \\'content\\': \\'He was one of the longest-lived birds on record[4] and was recognised by the Guinness World Records as the oldest living parrot in the world.[5]\\\\nThe next-oldest pink cockatoo to be found in a zoological setting was a 31-year-old female bird located at Paradise Wildlife Sanctuary, England.[3] Information published by the World Parrot Trust states longevity for Cookie\\\\\\'s species in captivity is on average 4060 years.[6]\\\\nLife[edit]\\\\nCookie was Brookfield Zoo\\\\\\'s oldest resident and the last surviving member of the animal collection from the time of the zoo\\\\\\'s opening in 1934, having arrived from Taronga Zoo of Sydney, New South Wales, Australia, in the same year and judged to be one year old at the time.[7]\\\\nIn the 1950s an attempt was made to introduce Cookie to a female pink cockatoo, but Cookie rejected her as \"she was not nice to him\".[8]\\\\n In 2007, Cookie was diagnosed with, and placed on medication and nutritional supplements for, osteoarthritis and osteoporosis\\\\xa0 medical conditions which occur commonly in aging animals and humans alike,[7] although it is believed that the latter may also have been brought on as a result of being fed a seed-only diet for the first 40 years of his life, in the years before the dietary requirements of his species were fully understood.[9]\\\\nCookie was \"retired\" from exhibition at the zoo in 2009 (following a few months of weekend-only appearances) in order to preserve his health, after it was noticed by staff that his appetite, demeanor and stress levels improved markedly when not on public display. age.[11] A memorial at the zoo was unveiled in September 2017.[12]\\\\nIn 2020, Cookie became the subject of a poetry collection by Barbara Gregorich entitled Cookie the Cockatoo: Everything Changes.[13]\\\\nSee also[edit]\\\\nReferences[edit]\\\\nExternal links[edit] He was believed to be the oldest member of his species alive in captivity, at the age of 82 in June 2015,[1][2] having significantly exceeded the average lifespan for his kind.[3] He was moved to a permanent residence in the keepers\\\\\\' office of the zoo\\\\\\'s Perching Bird House, although he made occasional appearances for special events, such as his birthday celebration, which was held each June.[3]\\'}]', additional_kwargs={'idx': 1, 'args': {'query': 'oldest parrot alive'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='[{\\'url\\': \\'https://www.thesprucepets.com/how-long-do-parrots-and-other-pet-birds-live-1238433\\', \\'content\\': \"It\\'s possible that a pet bird can outlive its owners\\\\nThe Spruce / Adrienne Legault\\\\nParrots and other birds can live up to 10 to 50 years or more depending on the type and the conditions they live in. They vary in size from small birds that can fit in the palm of your hand to large birds the size of a cat and their lifespans are just as variable.\\\\n Also, for birds who live longer some owners have to make a plan of where the bird is going in the circumstance the bird outlives the owner.\\\\n In reality, there is a wide range in the age that pet birds might reach and certainly, some will live longer (or shorter amounts of time) than the ages listed.\\\\n Potential owners need to be aware of the longevity of their bird so they can be prepared to provide proper care for them for as long as they live.\\\\n\"}]', additional_kwargs={'idx': 2, 'args': {'query': 'average lifespan of a parrot'}}, name='tavily_search_results_json', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n",
"---\n",
"{'join': [AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison.')]}\n",
"---\n",
"{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json')]}\n",
"---\n",
"{'join': [AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n",
"---\n",
"{'__end__': [HumanMessage(content=\"What's the oldest parrot alive, and how much longer is that than the average?\"), FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the Worlds Oldest Parrot? Discover the Worlds Oldest Parrot Advertisement of debate, so well detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join'), AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison. - Begin counting at : 3'), FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json'), AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: We have information on Cookie, the cockatoo, who was recognized as the oldest living parrot at 82 years old in June 2015. This significantly exceeds the average lifespan for his kind, which is stated to be 40-60 years. The second source provides a general lifespan range for parrots and other birds, which is 10-50 years. However, this range varies significantly depending on the species and conditions. Since Cookie's specific lifespan far exceeds the average for his species and falls outside the general range for parrots, we can answer the user's question.\", id='51a280ac-2327-40c5-a27a-c821697d5a4b'), AIMessage(content='The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.', id='139ecedf-b090-4197-88c0-0fa39883b392')]}}\n",
"---\n"
]
}
],
"source": [
"steps = chain.stream(\n",
"steps = chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n",
" )\n",
" ],\n",
" ]\n",
" },\n",
" {\n",
" \"recursion_limit\": 100,\n",
" },\n",
@@ -869,7 +862,7 @@
},
{
"cell_type": "code",
"execution_count": 21,
"execution_count": 94,
"id": "6c65c414-7668-4fdf-ba97-f42f659b1317",
"metadata": {},
"outputs": [
@@ -877,13 +870,13 @@
"name": "stdout",
"output_type": "stream",
"text": [
"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\n"
"The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.\n"
]
}
],
"source": [
"# Final answer\n",
"print(step[END][-1].content)"
"print(step['join']['messages'][-1].content)"
]
},
{
@@ -896,7 +889,7 @@
},
{
"cell_type": "code",
"execution_count": 22,
"execution_count": 96,
"id": "38d3ea91-59ba-4267-8060-ed75bbc840c6",
"metadata": {},
"outputs": [
@@ -904,26 +897,25 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'plan_and_schedule': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join')]}\n",
"{'join': [AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n",
"{'__end__': [HumanMessage(content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"), FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join'), AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n"
"{'plan_and_schedule': {'messages': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1, 'args': {'problem': '((3*(4+5)/0.5)+3245) + 8'}}, name='math', tool_call_id=1), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2, 'args': {'problem': '32/4.23'}}, name='math', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: The calculations for both individual questions have been provided: 3307.0 for the first equation and 7.565011820330969 for the second. To answer the user's final question, we need to sum these two values.\", id='96eb85f5-831f-434e-83d8-59deeebce05d'), AIMessage(content='The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.', id='671a1a08-4725-4f98-997a-848815d61aa5')]}}\n"
]
}
],
"source": [
"for step in chain.stream(\n",
"for step in chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n",
" )\n",
" ]\n",
" ]}\n",
"):\n",
" print(step)"
]
},
{
"cell_type": "code",
"execution_count": 23,
"execution_count": 97,
"id": "a6cf5fe0-f178-4197-950f-257711bff8d2",
"metadata": {
"scrolled": true
@@ -933,13 +925,53 @@
"name": "stdout",
"output_type": "stream",
"text": [
"The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.\n"
"The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.\n"
]
}
],
"source": [
"# Final answer\n",
"print(step[END][-1].content)"
"print(step['join']['messages'][-1].content)"
]
},
{
"cell_type": "markdown",
"id": "f9487866",
"metadata": {},
"source": [
"#### Complex Replanning Example\n",
"\n",
"This question is likely to prompt the Replan functionality, but it may need to be run multiple times to see this in action."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "391d6931",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.timeanddate.com/weather/japan/tokyo', 'content': '88 / 84 °F. 13. 87 / 82 °F. 14. 84 / 80 °F. Detailed forecast for 14 days. Need some help? Current weather in Tokyo and forecast for today, tomorrow, and next 14 days.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in Tokyo'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='join', additional_kwargs={'idx': 2, 'args': ()}, name='join', tool_call_id=2)]}}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides the current temperature in Tokyo but does not explicitly state which temperature (88 / 84 °F) corresponds to the current condition. It seems to be a range, possibly the day's high and low. Without a clear indication of the exact current temperature, it's challenging to provide a precise flashcard summary.\", id='8ef2a131-69db-4180-a76e-fd9d6f4037c1'), SystemMessage(content='Context from last attempt: The information provided does not explicitly state the current temperature in Tokyo; it provides a temperature range without specifying which is the current temperature. Need to find a source that gives the exact current temperature in Tokyo for a precise flashcard summary.', id='f5bd752c-b068-459a-8d9e-bd1f1b5fa4fe')]}}\n",
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='3cc41891-4f47-4453-8edf-b989926ab25e'), SystemMessage(content='Context from last attempt: The search did not provide an exact current temperature for Tokyo, making it impossible to create a precise flashcard. A source that explicitly states the current temperature is needed for an accurate response.', id='96290b41-a4c4-4ab5-829a-89cc31dfe6c8')]}}\n",
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 4, 'args': ()}, name='join', tool_call_id=4)]}}\n",
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='4724b242-ddb8-47e6-b235-de25de54fe45'), AIMessage(content='I was unable to find the exact current temperature in Tokyo. However, the temperature range for today in Tokyo is between 88°F and 84°F. For the most accurate and up-to-date temperature, I recommend checking a reliable weather forecasting website or app.', id='40e29a47-a001-4f65-a18f-65c2931d1ae5')]}}\n"
]
}
],
"source": [
"for step in chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n",
" )\n",
" ]}\n",
"):\n",
" print(step)"
]
},
{
@@ -955,14 +987,6 @@
"2. Variable substitution is fragile in the example above. It could be made more robust by using a fine-tuned model and a more robust syntax (using e.g., Lark or a tool calling schema)\n",
"3. The state can grow quite long if you require multiple re-planning runs. To handle, you could add a message compressor once you go above a certain token limit.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "431217e6-4c00-409f-a2bd-40ebff902489",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -981,7 +1005,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
"version": "3.11.9"
}
},
"nbformat": 4,
+1 -1
View File
@@ -114,7 +114,7 @@ def get_math_tool(llm: ChatOpenAI):
MessagesPlaceholder(variable_name="context", optional=True),
]
)
extractor = create_structured_output_runnable(ExecuteCode, llm, prompt)
extractor = prompt | llm.with_structured_output(ExecuteCode)
def calculate_expression(
problem: str,
+3 -3
View File
@@ -328,9 +328,9 @@
" \"set more_information_needed False and populate a blank string for the query.\"\n",
" )\n",
" input_messages = [system] + state[\"messages\"]\n",
" response = llm.bind_tools(\n",
" [QueryForTools], tool_choice=True\n",
" ).invoke(input_messages)\n",
" response = llm.bind_tools([QueryForTools], tool_choice=True).invoke(\n",
" input_messages\n",
" )\n",
" query = response.tool_calls[0][\"args\"][\"query\"]\n",
" tool_documents = vector_store.similarity_search(query)\n",
" if hack_remove_tool_condition:\n",
+24 -24
View File
@@ -25,7 +25,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "3eb04cd1",
"metadata": {},
"outputs": [],
@@ -36,10 +36,18 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"id": "dc292321",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"source": [
"import os\n",
"import getpass\n",
@@ -55,7 +63,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 3,
"id": "0f0f78e4-423d-4e2d-aa1a-01efaec4715f",
"metadata": {},
"outputs": [],
@@ -63,7 +71,7 @@
"import operator\n",
"from typing import Annotated, TypedDict\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"from langgraph.constants import Send\n",
@@ -87,7 +95,7 @@
"\n",
"\n",
"class BestJoke(BaseModel):\n",
" id: int\n",
" id: int = Field(description=\"Index of the best joke, starting with 0\")\n",
"\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
@@ -161,7 +169,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 4,
"id": "37ed1f71-63db-416f-b715-4617b33d4b7f",
"metadata": {},
"outputs": [
@@ -172,7 +180,7 @@
"<IPython.core.display.Image object>"
]
},
"execution_count": 11,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -185,7 +193,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 5,
"id": "fd90cace",
"metadata": {},
"outputs": [
@@ -193,12 +201,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"{'generate_topics': {'subjects': ['lion', 'elephant', 'penguin', 'dolphin']}}\n",
"{'generate_topics': {'subjects': ['Lions', 'Elephants', 'Penguins', 'Dolphins']}}\n",
"{'generate_joke': {'jokes': [\"Why don't elephants use computers? They're afraid of the mouse!\"]}}\n",
"{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? They're afraid of phishing!\"]}}\n",
"{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n",
"{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? Because they're afraid of phishing!\"]}}\n",
"{'generate_joke': {'jokes': [\"Why don't you see penguins in Britain? Because they're afraid of Wales!\"]}}\n",
"{'best_joke': {'best_selected_joke': \"Why don't you see penguins in Britain? Because they're afraid of Wales!\"}}\n"
"{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n",
"{'best_joke': {'best_selected_joke': \"Why don't dolphins use smartphones? Because they're afraid of phishing!\"}}\n"
]
}
],
@@ -207,21 +215,13 @@
"for s in app.stream({\"topic\": \"animals\"}):\n",
" print(s)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f28eaf56",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "langgraph",
"language": "python",
"name": "python3"
"name": "langgraph"
},
"language_info": {
"codemirror_mode": {
@@ -233,7 +233,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.8"
"version": "3.11.9"
}
},
"nbformat": 4,
@@ -508,14 +508,6 @@
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
" print_update(event)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "67d26013-1362-4cee-b135-ab5c3c4eb3d0",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+1 -9
View File
@@ -52,7 +52,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
@@ -466,14 +466,6 @@
"source": [
"Remember, when deleting messages you will want to make sure that the remaining message list is still valid. This message list **may actually not be** - this is because it currently starts with an AI message, which some models do not allow."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4d7222cd-5767-42f0-bc69-10615127eba5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -360,14 +360,6 @@
"- [How to filter messages](https://python.langchain.com/v0.2/docs/how_to/filter_messages/)\n",
"- [How to trim messages](https://python.langchain.com/v0.2/docs/how_to/trim_messages/)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "686861bb-ec32-46f3-b7b3-fdac106f22f6",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+162 -53
View File
@@ -26,7 +26,10 @@
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
"metadata": {},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"]
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"
]
},
{
"cell_type": "code",
@@ -34,7 +37,24 @@
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""
]
},
{
"cell_type": "markdown",
@@ -48,45 +68,51 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 1,
"id": "f04c6778-403b-4b49-9b93-678e910d5cec",
"metadata": {},
"outputs": [],
"source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_experimental.tools import PythonREPLTool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# This executes code locally, which can be unsafe\npython_repl_tool = PythonREPLTool()"]
"source": [
"from typing import Annotated\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_experimental.tools import PythonREPLTool\n",
"\n",
"tavily_tool = TavilySearchResults(max_results=5)\n",
"\n",
"# This executes code locally, which can be unsafe\n",
"python_repl_tool = PythonREPLTool()"
]
},
{
"cell_type": "markdown",
"id": "d58d1e85-22d4-4c22-9062-72a346a0d709",
"metadata": {},
"source": [
"## Helper Utilities\n",
"\n",
"Define a helper function below, which make it easier to add new agent worker nodes."
"## Helper Utilities"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "c4823dd9-26bd-4e1a-8117-b97b2860211a",
"metadata": {},
"outputs": [],
"source": ["from langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\n\ndef create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n # Each worker node will be given a name and some tools.\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_tools_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor"]
},
{
"cell_type": "markdown",
"id": "b7c302b0-cd57-4913-986f-5dc7d6d77386",
"metadata": {},
"source": [
"We can also define a function that we will use to be the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph"
"Define a helper function that we will use to create the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph"
]
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 2,
"id": "80862241-a1a7-4726-bce5-f867b233832e",
"metadata": {},
"outputs": [],
"source": ["def agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"]
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}"
]
},
{
"cell_type": "markdown",
@@ -100,11 +126,53 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 13,
"id": "311f0a58-b425-4496-adac-dc4cd8ffb912",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nmembers = [\"Researcher\", \"Coder\"]\nsystem_prompt = (\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\"\n)\n# Our team supervisor is an LLM node. It just picks the next agent to process\n# and decides when the work is completed\noptions = [\"FINISH\"] + members\n# Using openai function calling can make output parsing easier for us\nfunction_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n }\n },\n \"required\": [\"next\"],\n },\n}\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n).partial(options=str(options), members=\", \".join(members))\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_chain = (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n)"]
"source": [
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"from pydantic import BaseModel\n",
"from typing import Literal\n",
"\n",
"members = [\"Researcher\", \"Coder\"]\n",
"system_prompt = (\n",
" \"You are a supervisor tasked with managing a conversation between the\"\n",
" \" following workers: {members}. Given the following user request,\"\n",
" \" respond with the worker to act next. Each worker will perform a\"\n",
" \" task and respond with their results and status. When finished,\"\n",
" \" respond with FINISH.\"\n",
")\n",
"# Our team supervisor is an LLM node. It just picks the next agent to process\n",
"# and decides when the work is completed\n",
"options = [\"FINISH\"] + members\n",
"\n",
"class routeResponse(BaseModel):\n",
" next: Literal[*options]\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"system\",\n",
" \"Given the conversation above, who should act next?\"\n",
" \" Or should we FINISH? Select one of: {options}\",\n",
" ),\n",
" ]\n",
").partial(options=str(options), members=\", \".join(members))\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4o\")\n",
"\n",
"def supervisor_agent(state):\n",
" supervisor_chain = (\n",
" prompt\n",
" | llm.with_structured_output(routeResponse)\n",
" )\n",
" return supervisor_chain.invoke(state)"
]
},
{
"cell_type": "markdown",
@@ -118,11 +186,41 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 14,
"id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8",
"metadata": {},
"outputs": [],
"source": ["import functools\nimport operator\nfrom typing import Sequence, TypedDict\n\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\n# The agent state is the input to each node in the graph\nclass AgentState(TypedDict):\n # The annotation tells the graph that new messages will always\n # be added to the current states\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # The 'next' field indicates where to route to next\n next: str\n\n\nresearch_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\ncode_agent = create_agent(\n llm,\n [python_repl_tool],\n \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n)\ncode_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"Coder\", code_node)\nworkflow.add_node(\"supervisor\", supervisor_chain)"]
"source": [
"import functools\n",
"import operator\n",
"from typing import Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"# The agent state is the input to each node in the graph\n",
"class AgentState(TypedDict):\n",
" # The annotation tells the graph that new messages will always\n",
" # be added to the current states\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]\n",
" # The 'next' field indicates where to route to next\n",
" next: str\n",
"\n",
"\n",
"research_agent = create_react_agent(llm, tools=[tavily_tool])\n",
"research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n",
"\n",
"# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n",
"code_agent = create_react_agent(llm, tools=[python_repl_tool])\n",
"code_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n",
"\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"Researcher\", research_node)\n",
"workflow.add_node(\"Coder\", code_node)\n",
"workflow.add_node(\"supervisor\", supervisor_agent)"
]
},
{
"cell_type": "markdown",
@@ -134,11 +232,24 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 15,
"id": "14778e86-077b-4e6a-893c-400e59b0cdbf",
"metadata": {},
"outputs": [],
"source": ["for member in members:\n # We want our workers to ALWAYS \"report back\" to the supervisor when done\n workflow.add_edge(member, \"supervisor\")\n# The supervisor populates the \"next\" field in the graph state\n# which routes to a node or finishes\nconditional_map = {k: k for k in members}\nconditional_map[\"FINISH\"] = END\nworkflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n# Finally, add entrypoint\nworkflow.add_edge(START, \"supervisor\")\n\ngraph = workflow.compile()"]
"source": [
"for member in members:\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" workflow.add_edge(member, \"supervisor\")\n",
"# The supervisor populates the \"next\" field in the graph state\n",
"# which routes to a node or finishes\n",
"conditional_map = {k: k for k in members}\n",
"conditional_map[\"FINISH\"] = END\n",
"workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n",
"# Finally, add entrypoint\n",
"workflow.add_edge(START, \"supervisor\")\n",
"\n",
"graph = workflow.compile()"
]
},
{
"cell_type": "markdown",
@@ -152,7 +263,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 16,
"id": "56ba78e9-d9c1-457c-a073-d606d5d3e013",
"metadata": {},
"outputs": [
@@ -161,32 +272,30 @@
"output_type": "stream",
"text": [
"{'supervisor': {'next': 'Coder'}}\n",
"----\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Python REPL can execute arbitrary code. Use with caution.\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'Coder': {'messages': [HumanMessage(content=\"The code `print('Hello, World!')` was executed, and the output is:\\n\\n```\\nHello, World!\\n```\", name='Coder')]}}\n",
"----\n",
"{'Coder': {'messages': [HumanMessage(content='The code to print \"Hello, World!\" to the terminal is:\\n\\n```python\\nprint(\\'Hello, World!\\')\\n```\\n\\nWhen executed, it prints:\\n```\\nHello, World!\\n```', name='Coder')]}}\n",
"----\n",
"{'supervisor': {'next': 'FINISH'}}\n",
"----\n"
]
}
],
"source": ["for s in graph.stream(\n {\n \"messages\": [\n HumanMessage(content=\"Code hello world and print it to the terminal\")\n ]\n }\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"]
"source": [
"for s in graph.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(content=\"Code hello world and print it to the terminal\")\n",
" ]\n",
" }\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"----\")"
]
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 7,
"id": "45a92dfd-0e11-47f5-aad4-b68d24990e34",
"metadata": {},
"outputs": [
@@ -196,22 +305,22 @@
"text": [
"{'supervisor': {'next': 'Researcher'}}\n",
"----\n",
"{'Researcher': {'messages': [HumanMessage(content='**Research Report on Pikas**\\n\\nPikas are small mammals related to rabbits, known for their distinctive chirping sounds. They inhabit some of the most challenging environments, particularly boulder fields at high elevations, such as those found along the treeless slopes of the Southern Rockies, where they can be found at altitudes of up to 14,000 feet. Pikas are well-adapted to cold climates and typically do not fare well in warmer temperatures.\\n\\nRecent studies have shown that pikas are being impacted by climate change. Research by Peter Billman, a Ph.D. student from the University of Connecticut, indicates that pikas have moved upslope by approximately 1,160 feet. This upslope retreat is a direct response to changing climatic conditions, as pikas seek cooler temperatures at higher elevations.\\n\\nPikas are also known to be industrious foragers, particularly during the summer months when they gather vegetation to create haypiles for winter sustenance. Their behavior is encapsulated in the saying, \"making hay while the sun shines,\" reflecting their proactive approach to survival in harsh conditions.\\n\\nThe effects of climate change on pikas are not limited to the Southern Rockies. Studies published in Global Change Biology suggest that climate change is influencing pikas even in areas where they were previously thought to be less vulnerable, such as the Northern Rockies. These findings point to a broader trend of pikas moving to higher elevations, a behavior that may indicate a search for cooler, more suitable habitats.\\n\\nMoreover, researchers are exploring the possibility that pikas at lower elevations may have developed warm adaptations that could be beneficial for their future survival, given the ongoing climatic shifts. This line of research could help conservationists understand how pikas might cope with a warming world.\\n\\nIn conclusion, pikas are a species that not only fascinate with their unique behaviors and adaptations but also serve as indicators of environmental changes. Their upslope migration in response to climate change highlights the urgency for understanding and mitigating the effects of global warming on mountain ecosystems and the species that inhabit them.\\n\\n**Sources:**\\n- [Colorado Sun](https://coloradosun.com/2023/08/27/colorado-pika-population-climate-change/)\\n- [Wildlife.org](https://wildlife.org/climate-change-affects-pikas-even-in-unlikely-areas/)', name='Researcher')]}}\n",
"{'Researcher': {'messages': [HumanMessage(content='# Research Report on Pikas\\n\\nPikas, belonging to the genus Ochotona, are small, short-legged, and virtually tailless mammals that are often found in the mountains of western North America and across much of Asia. Despite their rodent-like appearance, pikas are not rodents but rather are part of the order Lagomorpha, which also includes rabbits and hares.\\n\\n## Behavior and Ecology\\nPikas are known for their unique behavior of not hibernating and remaining active throughout the winter. They navigate through tunnels under rocks and snow and rely on dried plants, which they have stored during warmer months in caches known as \"haypiles.\" This foraging strategy, termed \"haying,\" is crucial for their survival during the harsh winter months.\\n\\nPikas have a preference for cooler temperatures, typically foraging in temperatures below 25°C (77°F). They tend to avoid direct sunlight and stay in shaded regions when it gets warmer. A study has shown that for every 1°C (1.8°F) increase in ambient temperature, pikas can lose 3% of their foraging time, making them sensitive to climate change.\\n\\n## Distribution and Habitat\\nThe American pika (Ochotona princeps) and its relative, the collared pika (O. collaris), are found throughout the high mountainous regions of western North America. These species prefer cooler climates and have been observed to retreat to higher elevations as a response to increasing temperatures. Their current distribution is believed to be a result of a retreat from much larger ranges they occupied in the past, which included Western Europe and Eastern North America.\\n\\n## Conservation Status\\nThe International Union for Conservation of Nature and Natural Resources (IUCN) lists the American pika as a species of Least Concern but notes that populations are declining and unlikely to rebound due to habitat loss from extreme temperatures. The sensitivity of pikas to summer heat makes them an indicator species for the potential effects of climate change. Studies have shown that some populations are in decline, and there have been cases of local extirpation, particularly in the Great Basin.\\n\\n## Human Impact\\nHuman activity has impacted the ecosystems where pikas live, with recorded interactions dating back to the 1970s. Such interactions have been linked to pikas having reduced foraging time, limiting the amount of food they can stockpile for winter. Additionally, pikas have been considered pests in regions like the Tibetan plateau, where high densities of burrowing pikas are thought to reduce forage for domestic livestock and damage grasslands.\\n\\n## Conclusion\\nPikas are fascinating creatures with distinct adaptations that allow them to thrive in alpine environments. However, their future is uncertain due to the looming threats of climate change and habitat alteration. Conservation efforts, research, and monitoring are vital to ensure the survival of these unique mammals in a changing world.\\n\\n---\\n\\n**Sources:**\\n- [Wikipedia - Pika](https://en.wikipedia.org/wiki/Pika)\\n- [Treehugger - American Pika](https://www.treehugger.com/surprising-facts-about-american-pika-4864528)\\n- [National Park Service - Pikas at Rocky Mountain National Park](https://www.nps.gov/romo/learn/nature/pikas.htm)\\n- [Wikipedia - American Pika](https://en.wikipedia.org/wiki/American_pika)\\n- [Britannica - Pika](https://www.britannica.com/animal/pika)', name='Researcher')]}}\n",
"----\n",
"{'supervisor': {'next': 'FINISH'}}\n",
"----\n"
]
}
],
"source": ["for s in graph.stream(\n {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef",
"metadata": {},
"outputs": [],
"source": [""]
"source": [
"for s in graph.stream(\n",
" {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n",
" {\"recursion_limit\": 100},\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"----\")"
]
}
],
"metadata": {
@@ -230,7 +339,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -329,6 +329,7 @@
"\n",
"tools = [get_context, cite_context_sources]\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state, config):\n",
" messages = state[\"messages\"]\n",
+2 -10
View File
@@ -72,12 +72,12 @@
"# Node to retrieve documents\n",
"def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n",
" # Replace this with real logic\n",
" return {\"docs\": [state['query']] * 2}\n",
" return {\"docs\": [state[\"query\"]] * 2}\n",
"\n",
"\n",
"# Node to generate answer\n",
"def generate(state: GenerateInputState) -> OverallState:\n",
" return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n",
" return {\"answer\": \"\\n\\n\".join(state[\"docs\"] + [state[\"question\"]])}\n",
"\n",
"\n",
"graph = StateGraph(OverallState)\n",
@@ -92,14 +92,6 @@
"\n",
"graph.invoke({\"question\": \"foo\"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3ffc2d8c-717f-42c9-b0aa-15b178a5cc8b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+1 -9
View File
@@ -561,14 +561,6 @@
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb20430f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -587,7 +579,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.2"
}
},
"nbformat": 4,
+10 -4
View File
@@ -630,7 +630,7 @@
" upsert=True,\n",
" )\n",
" )\n",
" await self.db[\"checkpoint_writes\"].bulk_write(operations)\n"
" await self.db[\"checkpoint_writes\"].bulk_write(operations)"
]
},
{
@@ -685,7 +685,9 @@
"metadata": {},
"outputs": [],
"source": [
"with MongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n",
"with MongoDBSaver.from_conn_info(\n",
" host=\"localhost\", port=27017, db_name=\"checkpoints\"\n",
") as checkpointer:\n",
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
" config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
" res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n",
@@ -796,10 +798,14 @@
"metadata": {},
"outputs": [],
"source": [
"async with AsyncMongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n",
"async with AsyncMongoDBSaver.from_conn_info(\n",
" host=\"localhost\", port=27017, db_name=\"checkpoints\"\n",
") as checkpointer:\n",
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
" config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
" res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n",
" res = await graph.ainvoke(\n",
" {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n",
" )\n",
"\n",
" latest_checkpoint = await checkpointer.aget(config)\n",
" latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n",
+3 -3
View File
@@ -134,7 +134,7 @@
"source": [
"from psycopg.rows import dict_row\n",
"\n",
"connection_kwargs ={\n",
"connection_kwargs = {\n",
" \"autocommit\": True,\n",
" \"prepare_threshold\": 0,\n",
"}"
@@ -165,7 +165,7 @@
" # Example configuration\n",
" conninfo=DB_URI,\n",
" max_size=20,\n",
" kwargs=connection_kwargs\n",
" kwargs=connection_kwargs,\n",
")\n",
"\n",
"with pool.connection() as conn:\n",
@@ -393,7 +393,7 @@
" # Example configuration\n",
" conninfo=DB_URI,\n",
" max_size=20,\n",
" kwargs=connection_kwargs\n",
" kwargs=connection_kwargs,\n",
") as pool, pool.connection() as conn:\n",
" checkpointer = AsyncPostgresSaver(conn)\n",
"\n",
+9 -3
View File
@@ -530,7 +530,9 @@
"\n",
" @classmethod\n",
" @asynccontextmanager\n",
" async def from_conn_info(cls, *, host: str, port: int, db: int) -> AsyncIterator[\"AsyncRedisSaver\"]:\n",
" async def from_conn_info(\n",
" cls, *, host: str, port: int, db: int\n",
" ) -> AsyncIterator[\"AsyncRedisSaver\"]:\n",
" conn = None\n",
" try:\n",
" conn = AsyncRedis(host=host, port=port, db=db)\n",
@@ -887,10 +889,14 @@
"metadata": {},
"outputs": [],
"source": [
"async with AsyncRedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\n",
"async with AsyncRedisSaver.from_conn_info(\n",
" host=\"localhost\", port=6379, db=0\n",
") as checkpointer:\n",
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
" config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
" res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n",
" res = await graph.ainvoke(\n",
" {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n",
" )\n",
"\n",
" latest_checkpoint = await checkpointer.aget(config)\n",
" latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n",
+1 -9
View File
@@ -126,7 +126,7 @@
"source": [
"## Agent state\n",
" \n",
"We will defined a graph.\n",
"We will define a graph.\n",
"\n",
"A `state` object that it passes around to each node.\n",
"\n",
@@ -500,14 +500,6 @@
" pprint.pprint(value, indent=2, width=80, depth=None)\n",
" pprint.pprint(\"\\n---\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "189333cc-5d34-4869-9f9b-741210e1096f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -392,6 +392,7 @@
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"MAX_ITERATIONS = 5\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"draft\", first_responder.respond)\n",
@@ -246,14 +246,6 @@
"):\n",
" print(s)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20cac598",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
-8
View File
@@ -465,14 +465,6 @@
"for chunk in app.stream(inputs, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "296c7456-da05-4326-95dc-47d6b312da9d",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
-8
View File
@@ -439,14 +439,6 @@
"for chunk in app.stream(inputs, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "296c7456-da05-4326-95dc-47d6b312da9d",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+734 -38
View File
@@ -48,7 +48,10 @@
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python"
"%%capture --no-stderr\n",
"%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n",
"# We use one or the other search engine below\n",
"%pip install -U duckduckgo tavily-python"
]
},
{
@@ -57,7 +60,10 @@
"metadata": {},
"outputs": [],
"source": [
"# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz"
"# Uncomment if you want to draw the pretty graph diagrams.\n",
"# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n",
"# ! brew install graphviz\n",
"# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz"
]
},
{
@@ -66,7 +72,21 @@
"metadata": {},
"outputs": [],
"source": [
"import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")"
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var + \":\")\n",
"\n",
"\n",
"# Set for tracing\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
@@ -84,7 +104,12 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
"from langchain_openai import ChatOpenAI\n",
"\n",
"fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n",
"# Uncomment for a Fireworks model\n",
"# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n",
"long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
]
},
{
@@ -112,7 +137,64 @@
}
],
"source": [
"from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)"
"from typing import List, Optional\n",
"\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n",
" ),\n",
" (\"user\", \"{topic}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"class Subsection(BaseModel):\n",
" subsection_title: str = Field(..., title=\"Title of the subsection\")\n",
" description: str = Field(..., title=\"Content of the subsection\")\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n",
"\n",
"\n",
"class Section(BaseModel):\n",
" section_title: str = Field(..., title=\"Title of the section\")\n",
" description: str = Field(..., title=\"Content of the section\")\n",
" subsections: Optional[List[Subsection]] = Field(\n",
" default=None,\n",
" title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n",
" )\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" subsections = \"\\n\\n\".join(\n",
" f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n",
" for subsection in self.subsections or []\n",
" )\n",
" return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n",
"\n",
"\n",
"class Outline(BaseModel):\n",
" page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n",
" sections: List[Section] = Field(\n",
" default_factory=list,\n",
" title=\"Titles and descriptions for each section of the Wikipedia page.\",\n",
" )\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n",
" return f\"# {self.page_title}\\n\\n{sections}\".strip()\n",
"\n",
"\n",
"generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n",
" Outline\n",
")"
]
},
{
@@ -145,7 +227,11 @@
}
],
"source": [
"example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)"
"example_topic = \"Impact of million-plus token context window language models on RAG\"\n",
"\n",
"initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n",
"\n",
"print(initial_outline.as_str)"
]
},
{
@@ -165,7 +251,25 @@
"metadata": {},
"outputs": [],
"source": [
"gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)"
"gen_related_topics_prompt = ChatPromptTemplate.from_template(\n",
" \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n",
"\n",
"Please list the as many subjects and urls as you can.\n",
"\n",
"Topic of interest: {topic}\n",
"\"\"\"\n",
")\n",
"\n",
"\n",
"class RelatedSubjects(BaseModel):\n",
" topics: List[str] = Field(\n",
" description=\"Comprehensive list of related subjects as background research.\",\n",
" )\n",
"\n",
"\n",
"expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n",
" RelatedSubjects\n",
")"
]
},
{
@@ -185,7 +289,8 @@
}
],
"source": [
"related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects"
"related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n",
"related_subjects"
]
},
{
@@ -204,7 +309,49 @@
"metadata": {},
"outputs": [],
"source": [
"class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)"
"class Editor(BaseModel):\n",
" affiliation: str = Field(\n",
" description=\"Primary affiliation of the editor.\",\n",
" )\n",
" name: str = Field(\n",
" description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n",
" )\n",
" role: str = Field(\n",
" description=\"Role of the editor in the context of the topic.\",\n",
" )\n",
" description: str = Field(\n",
" description=\"Description of the editor's focus, concerns, and motives.\",\n",
" )\n",
"\n",
" @property\n",
" def persona(self) -> str:\n",
" return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n",
"\n",
"\n",
"class Perspectives(BaseModel):\n",
" editors: List[Editor] = Field(\n",
" description=\"Comprehensive list of editors with their roles and affiliations.\",\n",
" # Add a pydantic validation/restriction to be at most M editors\n",
" )\n",
"\n",
"\n",
"gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n",
" You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n",
"\n",
" Wiki page outlines of related topics for inspiration:\n",
" {examples}\"\"\",\n",
" ),\n",
" (\"user\", \"Topic of interest: {topic}\"),\n",
" ]\n",
")\n",
"\n",
"gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n",
" model=\"gpt-3.5-turbo\"\n",
").with_structured_output(Perspectives)"
]
},
{
@@ -213,7 +360,37 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})"
"from langchain_community.retrievers import WikipediaRetriever\n",
"from langchain_core.runnables import RunnableLambda\n",
"from langchain_core.runnables import chain as as_runnable\n",
"\n",
"wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n",
"\n",
"\n",
"def format_doc(doc, max_length=1000):\n",
" related = \"- \".join(doc.metadata[\"categories\"])\n",
" return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n",
" :max_length\n",
" ]\n",
"\n",
"\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n",
"\n",
"\n",
"@as_runnable\n",
"async def survey_subjects(topic: str):\n",
" related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n",
" retrieved_docs = await wikipedia_retriever.abatch(\n",
" related_subjects.topics, return_exceptions=True\n",
" )\n",
" all_docs = []\n",
" for docs in retrieved_docs:\n",
" if isinstance(docs, BaseException):\n",
" continue\n",
" all_docs.extend(docs)\n",
" formatted = format_docs(all_docs)\n",
" return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})"
]
},
{
@@ -280,7 +457,40 @@
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]"
"from typing import Annotated\n",
"\n",
"from langchain_core.messages import AnyMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"\n",
"def add_messages(left, right):\n",
" if not isinstance(left, list):\n",
" left = [left]\n",
" if not isinstance(right, list):\n",
" right = [right]\n",
" return left + right\n",
"\n",
"\n",
"def update_references(references, new_references):\n",
" if not references:\n",
" references = {}\n",
" references.update(new_references)\n",
" return references\n",
"\n",
"\n",
"def update_editor(editor, new_editor):\n",
" # Can only set at the outset\n",
" if not editor:\n",
" return new_editor\n",
" return editor\n",
"\n",
"\n",
"class InterviewState(TypedDict):\n",
" messages: Annotated[List[AnyMessage], add_messages]\n",
" references: Annotated[Optional[dict], update_references]\n",
" editor: Annotated[Optional[Editor], update_editor]"
]
},
{
@@ -298,7 +508,56 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}"
"from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n",
"from langchain_core.prompts import MessagesPlaceholder\n",
"\n",
"gen_qn_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n",
"Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n",
"Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n",
"\n",
"When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n",
"Please only ask one question at a time and don't ask what you have asked before.\\\n",
"Your questions should be related to the topic you want to write.\n",
"Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n",
"\n",
"Stay true to your specific perspective:\n",
"\n",
"{persona}\"\"\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
" ]\n",
")\n",
"\n",
"\n",
"def tag_with_name(ai_message: AIMessage, name: str):\n",
" ai_message.name = name\n",
" return ai_message\n",
"\n",
"\n",
"def swap_roles(state: InterviewState, name: str):\n",
" converted = []\n",
" for message in state[\"messages\"]:\n",
" if isinstance(message, AIMessage) and message.name != name:\n",
" message = HumanMessage(**message.dict(exclude={\"type\"}))\n",
" converted.append(message)\n",
" return {\"messages\": converted}\n",
"\n",
"\n",
"@as_runnable\n",
"async def generate_question(state: InterviewState):\n",
" editor = state[\"editor\"]\n",
" gn_chain = (\n",
" RunnableLambda(swap_roles).bind(name=editor.name)\n",
" | gen_qn_prompt.partial(persona=editor.persona)\n",
" | fast_llm\n",
" | RunnableLambda(tag_with_name).bind(name=editor.name)\n",
" )\n",
" result = await gn_chain.ainvoke(state)\n",
" return {\"messages\": [result]}"
]
},
{
@@ -318,7 +577,17 @@
}
],
"source": [
"messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content"
"messages = [\n",
" HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n",
"]\n",
"question = await generate_question.ainvoke(\n",
" {\n",
" \"editor\": perspectives.editors[0],\n",
" \"messages\": messages,\n",
" }\n",
")\n",
"\n",
"question[\"messages\"][0].content"
]
},
{
@@ -336,7 +605,24 @@
"metadata": {},
"outputs": [],
"source": [
"class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)"
"class Queries(BaseModel):\n",
" queries: List[str] = Field(\n",
" description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n",
" )\n",
"\n",
"\n",
"gen_queries_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
" ]\n",
")\n",
"gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n",
" model=\"gpt-3.5-turbo\"\n",
").with_structured_output(Queries, include_raw=True)"
]
},
{
@@ -357,7 +643,10 @@
}
],
"source": [
"queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries"
"queries = await gen_queries_chain.ainvoke(\n",
" {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n",
")\n",
"queries[\"parsed\"].queries"
]
},
{
@@ -366,7 +655,38 @@
"metadata": {},
"outputs": [],
"source": [
"class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")"
"class AnswerWithCitations(BaseModel):\n",
" answer: str = Field(\n",
" description=\"Comprehensive answer to the user's question with citations.\",\n",
" )\n",
" cited_urls: List[str] = Field(\n",
" description=\"List of urls cited in the answer.\",\n",
" )\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n",
" f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n",
" )\n",
"\n",
"\n",
"gen_answer_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n",
" to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n",
"\n",
"Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n",
"Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
" ]\n",
")\n",
"\n",
"gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n",
" AnswerWithCitations, include_raw=True\n",
").with_config(run_name=\"GenerateAnswer\")"
]
},
{
@@ -375,7 +695,29 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]"
"from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n",
"from langchain_core.tools import tool\n",
"\n",
"'''\n",
"# Tavily is typically a better search engine, but your free queries are limited\n",
"search_engine = TavilySearchResults(max_results=4)\n",
"\n",
"@tool\n",
"async def search_engine(query: str):\n",
" \"\"\"Search engine to the internet.\"\"\"\n",
" results = tavily_search.invoke(query)\n",
" return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n",
"'''\n",
"\n",
"# DDG\n",
"search_engine = DuckDuckGoSearchAPIWrapper()\n",
"\n",
"\n",
"@tool\n",
"async def search_engine(query: str):\n",
" \"\"\"Search engine to the internet.\"\"\"\n",
" results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n",
" return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]"
]
},
{
@@ -384,7 +726,43 @@
"metadata": {},
"outputs": [],
"source": [
"import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}"
"import json\n",
"\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"\n",
"async def gen_answer(\n",
" state: InterviewState,\n",
" config: Optional[RunnableConfig] = None,\n",
" name: str = \"Subject_Matter_Expert\",\n",
" max_str_len: int = 15000,\n",
"):\n",
" swapped_state = swap_roles(state, name) # Convert all other AI messages\n",
" queries = await gen_queries_chain.ainvoke(swapped_state)\n",
" query_results = await search_engine.abatch(\n",
" queries[\"parsed\"].queries, config, return_exceptions=True\n",
" )\n",
" successful_results = [\n",
" res for res in query_results if not isinstance(res, Exception)\n",
" ]\n",
" all_query_results = {\n",
" res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n",
" }\n",
" # We could be more precise about handling max token length if we wanted to here\n",
" dumped = json.dumps(all_query_results)[:max_str_len]\n",
" ai_message: AIMessage = queries[\"raw\"]\n",
" tool_call = queries[\"raw\"].tool_calls[0]\n",
" tool_id = tool_call[\"id\"]\n",
" tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n",
" swapped_state[\"messages\"].extend([ai_message, tool_message])\n",
" # Only update the shared state with the final answer to avoid\n",
" # polluting the dialogue history with intermediate messages\n",
" generated = await gen_answer_chain.ainvoke(swapped_state)\n",
" cited_urls = set(generated[\"parsed\"].cited_urls)\n",
" # Save the retrieved information to a the shared state for future reference\n",
" cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n",
" formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n",
" return {\"messages\": [formatted_message], \"references\": cited_references}"
]
},
{
@@ -404,7 +782,10 @@
}
],
"source": [
"example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content"
"example_answer = await gen_answer(\n",
" {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n",
")\n",
"example_answer[\"messages\"][-1].content"
]
},
{
@@ -423,7 +804,31 @@
"metadata": {},
"outputs": [],
"source": [
"max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")"
"max_num_turns = 5\n",
"\n",
"\n",
"def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n",
" messages = state[\"messages\"]\n",
" num_responses = len(\n",
" [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n",
" )\n",
" if num_responses >= max_num_turns:\n",
" return END\n",
" last_question = messages[-2]\n",
" if last_question.content.endswith(\"Thank you so much for your help!\"):\n",
" return END\n",
" return \"ask_question\"\n",
"\n",
"\n",
"builder = StateGraph(InterviewState)\n",
"\n",
"builder.add_node(\"ask_question\", generate_question)\n",
"builder.add_node(\"answer_question\", gen_answer)\n",
"builder.add_conditional_edges(\"answer_question\", route_messages)\n",
"builder.add_edge(\"ask_question\", \"answer_question\")\n",
"\n",
"builder.add_edge(START, \"ask_question\")\n",
"interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")"
]
},
{
@@ -444,7 +849,11 @@
}
],
"source": [
"from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())"
"from IPython.display import Image\n",
"\n",
"# Feel free to comment out if you have\n",
"# not installed pygraphviz\n",
"Image(interview_graph.get_graph().draw_png())"
]
},
{
@@ -474,7 +883,23 @@
}
],
"source": [
"final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step"
"final_step = None\n",
"\n",
"initial_state = {\n",
" \"editor\": perspectives.editors[0],\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=f\"So you said you were writing an article on {example_topic}?\",\n",
" name=\"Subject_Matter_Expert\",\n",
" )\n",
" ],\n",
"}\n",
"async for step in interview_graph.astream(initial_state):\n",
" name = next(iter(step))\n",
" print(name)\n",
" print(\"-- \", str(step[name][\"messages\"])[:300])\n",
" if END in step:\n",
" final_step = step"
]
},
{
@@ -501,7 +926,29 @@
"metadata": {},
"outputs": [],
"source": [
"refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)"
"refine_outline_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n",
"You need to make sure that the outline is comprehensive and specific. \\\n",
"Topic you are writing about: {topic} \n",
"\n",
"Old outline:\n",
"\n",
"{old_outline}\"\"\",\n",
" ),\n",
" (\n",
" \"user\",\n",
" \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
"# Using turbo preview since the context can get quite long\n",
"refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n",
" Outline\n",
")"
]
},
{
@@ -510,7 +957,15 @@
"metadata": {},
"outputs": [],
"source": [
"refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)"
"refined_outline = refine_outline_chain.invoke(\n",
" {\n",
" \"topic\": example_topic,\n",
" \"old_outline\": initial_outline.as_str,\n",
" \"conversations\": \"\\n\\n\".join(\n",
" f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n",
" ),\n",
" }\n",
")"
]
},
{
@@ -595,7 +1050,23 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)"
"from langchain_community.vectorstores import SKLearnVectorStore\n",
"from langchain_core.documents import Document\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
"reference_docs = [\n",
" Document(page_content=v, metadata={\"source\": k})\n",
" for k, v in final_state[\"references\"].items()\n",
"]\n",
"# This really doesn't need to be a vectorstore for this size of data.\n",
"# It could just be a numpy matrix. Or you could store documents\n",
"# across requests if you want.\n",
"vectorstore = SKLearnVectorStore.from_documents(\n",
" reference_docs,\n",
" embedding=embeddings,\n",
")\n",
"retriever = vectorstore.as_retriever(k=10)"
]
},
{
@@ -636,7 +1107,67 @@
"metadata": {},
"outputs": [],
"source": [
"class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n<Documents>\\n{docs}\\n<Documents>\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'<Document href=\"{doc.metadata[\"source\"]}\"/>\\n{doc.page_content}\\n</Document>'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)"
"class SubSection(BaseModel):\n",
" subsection_title: str = Field(..., title=\"Title of the subsection\")\n",
" content: str = Field(\n",
" ...,\n",
" title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n",
" )\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n",
"\n",
"\n",
"class WikiSection(BaseModel):\n",
" section_title: str = Field(..., title=\"Title of the section\")\n",
" content: str = Field(..., title=\"Full content of the section\")\n",
" subsections: Optional[List[Subsection]] = Field(\n",
" default=None,\n",
" title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n",
" )\n",
" citations: List[str] = Field(default_factory=list)\n",
"\n",
" @property\n",
" def as_str(self) -> str:\n",
" subsections = \"\\n\\n\".join(\n",
" subsection.as_str for subsection in self.subsections or []\n",
" )\n",
" citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n",
" return (\n",
" f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n",
" + f\"\\n\\n{citations}\".strip()\n",
" )\n",
"\n",
"\n",
"section_writer_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n",
" \"{outline}\\n\\nCite your sources, using the following references:\\n\\n<Documents>\\n{docs}\\n<Documents>\",\n",
" ),\n",
" (\"user\", \"Write the full WikiSection for the {section} section.\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"async def retrieve(inputs: dict):\n",
" docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n",
" formatted = \"\\n\".join(\n",
" [\n",
" f'<Document href=\"{doc.metadata[\"source\"]}\"/>\\n{doc.page_content}\\n</Document>'\n",
" for doc in docs\n",
" ]\n",
" )\n",
" return {\"docs\": formatted, **inputs}\n",
"\n",
"\n",
"section_writer = (\n",
" retrieve\n",
" | section_writer_prompt\n",
" | long_context_llm.with_structured_output(WikiSection)\n",
")"
]
},
{
@@ -663,7 +1194,14 @@
}
],
"source": [
"section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)"
"section = await section_writer.ainvoke(\n",
" {\n",
" \"outline\": refined_outline.as_str,\n",
" \"section\": refined_outline.sections[1].section_title,\n",
" \"topic\": example_topic,\n",
" }\n",
")\n",
"print(section.as_str)"
]
},
{
@@ -681,7 +1219,24 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()"
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"writer_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n",
" \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n",
" ),\n",
" (\n",
" \"user\",\n",
" 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n",
" \" avoiding duplicates in the footer. Include URLs in the footer.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
"writer = writer_prompt | long_context_llm | StrOutputParser()"
]
},
{
@@ -774,7 +1329,8 @@
}
],
"source": [
"for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")"
"for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n",
" print(tok, end=\"\")"
]
},
{
@@ -801,7 +1357,14 @@
"metadata": {},
"outputs": [],
"source": [
"class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str"
"class ResearchState(TypedDict):\n",
" topic: str\n",
" outline: Outline\n",
" editors: List[Editor]\n",
" interview_results: List[InterviewState]\n",
" # The final sections output\n",
" sections: List[WikiSection]\n",
" article: str"
]
},
{
@@ -810,7 +1373,109 @@
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }"
"import asyncio\n",
"\n",
"\n",
"async def initialize_research(state: ResearchState):\n",
" topic = state[\"topic\"]\n",
" coros = (\n",
" generate_outline_direct.ainvoke({\"topic\": topic}),\n",
" survey_subjects.ainvoke(topic),\n",
" )\n",
" results = await asyncio.gather(*coros)\n",
" return {\n",
" **state,\n",
" \"outline\": results[0],\n",
" \"editors\": results[1].editors,\n",
" }\n",
"\n",
"\n",
"async def conduct_interviews(state: ResearchState):\n",
" topic = state[\"topic\"]\n",
" initial_states = [\n",
" {\n",
" \"editor\": editor,\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=f\"So you said you were writing an article on {topic}?\",\n",
" name=\"Subject_Matter_Expert\",\n",
" )\n",
" ],\n",
" }\n",
" for editor in state[\"editors\"]\n",
" ]\n",
" # We call in to the sub-graph here to parallelize the interviews\n",
" interview_results = await interview_graph.abatch(initial_states)\n",
"\n",
" return {\n",
" **state,\n",
" \"interview_results\": interview_results,\n",
" }\n",
"\n",
"\n",
"def format_conversation(interview_state):\n",
" messages = interview_state[\"messages\"]\n",
" convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n",
" return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n",
"\n",
"\n",
"async def refine_outline(state: ResearchState):\n",
" convos = \"\\n\\n\".join(\n",
" [\n",
" format_conversation(interview_state)\n",
" for interview_state in state[\"interview_results\"]\n",
" ]\n",
" )\n",
"\n",
" updated_outline = await refine_outline_chain.ainvoke(\n",
" {\n",
" \"topic\": state[\"topic\"],\n",
" \"old_outline\": state[\"outline\"].as_str,\n",
" \"conversations\": convos,\n",
" }\n",
" )\n",
" return {**state, \"outline\": updated_outline}\n",
"\n",
"\n",
"async def index_references(state: ResearchState):\n",
" all_docs = []\n",
" for interview_state in state[\"interview_results\"]:\n",
" reference_docs = [\n",
" Document(page_content=v, metadata={\"source\": k})\n",
" for k, v in interview_state[\"references\"].items()\n",
" ]\n",
" all_docs.extend(reference_docs)\n",
" await vectorstore.aadd_documents(all_docs)\n",
" return state\n",
"\n",
"\n",
"async def write_sections(state: ResearchState):\n",
" outline = state[\"outline\"]\n",
" sections = await section_writer.abatch(\n",
" [\n",
" {\n",
" \"outline\": refined_outline.as_str,\n",
" \"section\": section.section_title,\n",
" \"topic\": state[\"topic\"],\n",
" }\n",
" for section in outline.sections\n",
" ]\n",
" )\n",
" return {\n",
" **state,\n",
" \"sections\": sections,\n",
" }\n",
"\n",
"\n",
"async def write_article(state: ResearchState):\n",
" topic = state[\"topic\"]\n",
" sections = state[\"sections\"]\n",
" draft = \"\\n\\n\".join([section.as_str for section in sections])\n",
" article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n",
" return {\n",
" **state,\n",
" \"article\": article,\n",
" }"
]
},
{
@@ -826,7 +1491,27 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())"
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"builder_of_storm = StateGraph(ResearchState)\n",
"\n",
"nodes = [\n",
" (\"init_research\", initialize_research),\n",
" (\"conduct_interviews\", conduct_interviews),\n",
" (\"refine_outline\", refine_outline),\n",
" (\"index_references\", index_references),\n",
" (\"write_sections\", write_sections),\n",
" (\"write_article\", write_article),\n",
"]\n",
"for i in range(len(nodes)):\n",
" name, node = nodes[i]\n",
" builder_of_storm.add_node(name, node)\n",
" if i > 0:\n",
" builder_of_storm.add_edge(nodes[i - 1][0], name)\n",
"\n",
"builder_of_storm.add_edge(START, nodes[0][0])\n",
"builder_of_storm.add_edge(nodes[-1][0], END)\n",
"storm = builder_of_storm.compile(checkpointer=MemorySaver())"
]
},
{
@@ -877,7 +1562,16 @@
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])"
"config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n",
"async for step in storm.astream(\n",
" {\n",
" \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n",
" },\n",
" config,\n",
"):\n",
" name = next(iter(step))\n",
" print(name)\n",
" print(\"-- \", str(step[name])[:300])"
]
},
{
@@ -886,7 +1580,8 @@
"metadata": {},
"outputs": [],
"source": [
"checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]"
"checkpoint = storm.get_state(config)\n",
"article = checkpoint.values[\"article\"]"
]
},
{
@@ -967,7 +1662,10 @@
}
],
"source": [
"from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))"
"from IPython.display import Markdown\n",
"\n",
"# We will down-header the sections to create less confusion in this notebook\n",
"Markdown(article.replace(\"\\n#\", \"\\n##\"))"
]
},
{
@@ -975,9 +1673,7 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
""
]
"source": []
}
],
"metadata": {
+1 -9
View File
@@ -50,7 +50,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
@@ -175,14 +175,6 @@
" print(chunk)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8cc57240-243c-4d9a-a845-cb55ef973a59",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+1 -9
View File
@@ -55,7 +55,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
@@ -151,14 +151,6 @@
" print(values)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8cc57240-243c-4d9a-a845-cb55ef973a59",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+3 -9
View File
@@ -68,7 +68,9 @@
" # It's completely optional, but useful if you have many functions with similar names\n",
" gen = RunnableGenerator(my_generator).with_config(\n",
" tags=[\"should_stream\"],\n",
" callbacks=config.get(\"callbacks\", []) # <-- Propagate callbacks (Python <= 3.10)\n",
" callbacks=config.get(\n",
" \"callbacks\", []\n",
" ), # <-- Propagate callbacks (Python <= 3.10)\n",
" )\n",
" async for message in gen.astream(state):\n",
" messages.append(message)\n",
@@ -118,14 +120,6 @@
" # So we only print non-empty content\n",
" print(data, end=\"|\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "615cb9d2-bfa2-4f83-90b0-c6c2d1e6df95",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -30,10 +30,7 @@
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph openai"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph openai"]
},
{
"cell_type": "code",
@@ -49,18 +46,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -84,94 +70,7 @@
"id": "d59234f9-173e-469d-a725-c13e0979663e",
"metadata": {},
"outputs": [],
"source": [
"from openai import AsyncOpenAI\n",
"from langchain_core.language_models.chat_models import ChatGenerationChunk\n",
"from langchain_core.messages import AIMessageChunk\n",
"from langchain_core.runnables.config import (\n",
" ensure_config,\n",
" get_callback_manager_for_config,\n",
")\n",
"\n",
"openai_client = AsyncOpenAI()\n",
"# define tool schema for openai tool calling\n",
"\n",
"tool = {\n",
" \"type\": \"function\",\n",
" \"function\": {\n",
" \"name\": \"get_items\",\n",
" \"description\": \"Use this tool to look up which items are in the given place.\",\n",
" \"parameters\": {\n",
" \"type\": \"object\",\n",
" \"properties\": {\"place\": {\"type\": \"string\"}},\n",
" \"required\": [\"place\"],\n",
" },\n",
" },\n",
"}\n",
"\n",
"\n",
"async def call_model(state, config=None):\n",
" config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n",
" callback_manager = get_callback_manager_for_config(config)\n",
" messages = state[\"messages\"]\n",
"\n",
" llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n",
" response = await openai_client.chat.completions.create(\n",
" messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n",
" )\n",
"\n",
" response_content = \"\"\n",
" role = None\n",
"\n",
" tool_call_id = None\n",
" tool_call_function_name = None\n",
" tool_call_function_arguments = \"\"\n",
" async for chunk in response:\n",
" delta = chunk.choices[0].delta\n",
" if delta.role is not None:\n",
" role = delta.role\n",
"\n",
" if delta.content:\n",
" response_content += delta.content\n",
" llm_run_manager.on_llm_new_token(delta.content)\n",
"\n",
" if delta.tool_calls:\n",
" # note: for simplicity we're only handling a single tool call here\n",
" if delta.tool_calls[0].function.name is not None:\n",
" tool_call_function_name = delta.tool_calls[0].function.name\n",
" tool_call_id = delta.tool_calls[0].id\n",
"\n",
" # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n",
" tool_call_chunk = ChatGenerationChunk(\n",
" message=AIMessageChunk(\n",
" content=\"\",\n",
" additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n",
" )\n",
" )\n",
" llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n",
" tool_call_function_arguments += delta.tool_calls[0].function.arguments\n",
"\n",
" if tool_call_function_name is not None:\n",
" tool_calls = [\n",
" {\n",
" \"id\": tool_call_id,\n",
" \"function\": {\n",
" \"name\": tool_call_function_name,\n",
" \"arguments\": tool_call_function_arguments,\n",
" },\n",
" \"type\": \"function\",\n",
" }\n",
" ]\n",
" else:\n",
" tool_calls = None\n",
"\n",
" response_message = {\n",
" \"role\": role,\n",
" \"content\": response_content,\n",
" \"tool_calls\": tool_calls,\n",
" }\n",
" return {\"messages\": [response_message]}"
]
"source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"]
},
{
"cell_type": "markdown",
@@ -187,62 +86,7 @@
"id": "b90941d8-afe4-42ec-9262-9c3b87c3b1ec",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from langchain_core.callbacks import adispatch_custom_event\n",
"\n",
"\n",
"async def get_items(place: str) -> str:\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
"\n",
" # this can be replaced with any actual streaming logic that you might have\n",
" def stream(place: str):\n",
" if \"bed\" in place: # For under the bed\n",
" yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n",
" elif \"shelf\" in place: # For 'shelf'\n",
" yield from [\"books\", \"penciles\", \"pictures\"]\n",
" else: # if the agent decides to ask about a different place\n",
" yield \"cat snacks\"\n",
"\n",
" tokens = []\n",
" for token in stream(place):\n",
" await adispatch_custom_event(\n",
" # this will allow you to filter events by name\n",
" \"tool_call_token_stream\",\n",
" {\n",
" \"function_name\": \"get_items\",\n",
" \"arguments\": {\"place\": place},\n",
" \"tool_output_token\": token,\n",
" },\n",
" # this will allow you to filter events by tags\n",
" config={\"tags\": [\"tool_call\"]},\n",
" )\n",
" tokens.append(token)\n",
"\n",
" return \", \".join(tokens)\n",
"\n",
"\n",
"# define mapping to look up functions when running tools\n",
"function_name_to_function = {\"get_items\": get_items}\n",
"\n",
"\n",
"async def call_tools(state):\n",
" messages = state[\"messages\"]\n",
"\n",
" tool_call = messages[-1][\"tool_calls\"][0]\n",
" function_name = tool_call[\"function\"][\"name\"]\n",
" function_arguments = tool_call[\"function\"][\"arguments\"]\n",
" arguments = json.loads(function_arguments)\n",
"\n",
" function_response = await function_name_to_function[function_name](**arguments)\n",
" tool_message = {\n",
" \"tool_call_id\": tool_call[\"id\"],\n",
" \"role\": \"tool\",\n",
" \"name\": function_name,\n",
" \"content\": function_response,\n",
" }\n",
" return {\"messages\": [tool_message]}"
]
"source": ["import json\nfrom langchain_core.callbacks import adispatch_custom_event\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n\n # this can be replaced with any actual streaming logic that you might have\n def stream(place: str):\n if \"bed\" in place: # For under the bed\n yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n elif \"shelf\" in place: # For 'shelf'\n yield from [\"books\", \"penciles\", \"pictures\"]\n else: # if the agent decides to ask about a different place\n yield \"cat snacks\"\n\n tokens = []\n for token in stream(place):\n await adispatch_custom_event(\n # this will allow you to filter events by name\n \"tool_call_token_stream\",\n {\n \"function_name\": \"get_items\",\n \"arguments\": {\"place\": place},\n \"tool_output_token\": token,\n },\n # this will allow you to filter events by tags\n config={\"tags\": [\"tool_call\"]},\n )\n tokens.append(token)\n\n return \", \".join(tokens)\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"]
},
{
"cell_type": "markdown",
@@ -258,33 +102,7 @@
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Literal\n",
"\n",
"from langgraph.graph import StateGraph, END\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, operator.add]\n",
"\n",
"\n",
"def should_continue(state) -> Literal[\"tools\", END]:\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" if last_message[\"tool_calls\"]:\n",
" return \"tools\"\n",
" return END\n",
"\n",
"\n",
"workflow = StateGraph(State)\n",
"workflow.set_entry_point(\"model\")\n",
"workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n",
"workflow.add_node(\"tools\", call_tools)\n",
"workflow.add_conditional_edges(\"model\", should_continue)\n",
"workflow.add_edge(\"tools\", \"model\")\n",
"graph = workflow.compile()"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"]
},
{
"cell_type": "markdown",
@@ -318,14 +136,7 @@
]
}
],
"source": [
"async for event in graph.astream_events(\n",
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n",
"):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n",
" print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"
]
"source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
@@ -30,10 +30,7 @@
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph openai"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph openai"]
},
{
"cell_type": "code",
@@ -49,18 +46,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -84,94 +70,7 @@
"id": "d59234f9-173e-469d-a725-c13e0979663e",
"metadata": {},
"outputs": [],
"source": [
"from openai import AsyncOpenAI\n",
"from langchain_core.language_models.chat_models import ChatGenerationChunk\n",
"from langchain_core.messages import AIMessageChunk\n",
"from langchain_core.runnables.config import (\n",
" ensure_config,\n",
" get_callback_manager_for_config,\n",
")\n",
"\n",
"openai_client = AsyncOpenAI()\n",
"# define tool schema for openai tool calling\n",
"\n",
"tool = {\n",
" \"type\": \"function\",\n",
" \"function\": {\n",
" \"name\": \"get_items\",\n",
" \"description\": \"Use this tool to look up which items are in the given place.\",\n",
" \"parameters\": {\n",
" \"type\": \"object\",\n",
" \"properties\": {\"place\": {\"type\": \"string\"}},\n",
" \"required\": [\"place\"],\n",
" },\n",
" },\n",
"}\n",
"\n",
"\n",
"async def call_model(state, config=None):\n",
" config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n",
" callback_manager = get_callback_manager_for_config(config)\n",
" messages = state[\"messages\"]\n",
"\n",
" llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n",
" response = await openai_client.chat.completions.create(\n",
" messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n",
" )\n",
"\n",
" response_content = \"\"\n",
" role = None\n",
"\n",
" tool_call_id = None\n",
" tool_call_function_name = None\n",
" tool_call_function_arguments = \"\"\n",
" async for chunk in response:\n",
" delta = chunk.choices[0].delta\n",
" if delta.role is not None:\n",
" role = delta.role\n",
"\n",
" if delta.content:\n",
" response_content += delta.content\n",
" llm_run_manager.on_llm_new_token(delta.content)\n",
"\n",
" if delta.tool_calls:\n",
" # note: for simplicity we're only handling a single tool call here\n",
" if delta.tool_calls[0].function.name is not None:\n",
" tool_call_function_name = delta.tool_calls[0].function.name\n",
" tool_call_id = delta.tool_calls[0].id\n",
"\n",
" # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n",
" tool_call_chunk = ChatGenerationChunk(\n",
" message=AIMessageChunk(\n",
" content=\"\",\n",
" additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n",
" )\n",
" )\n",
" llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n",
" tool_call_function_arguments += delta.tool_calls[0].function.arguments\n",
"\n",
" if tool_call_function_name is not None:\n",
" tool_calls = [\n",
" {\n",
" \"id\": tool_call_id,\n",
" \"function\": {\n",
" \"name\": tool_call_function_name,\n",
" \"arguments\": tool_call_function_arguments,\n",
" },\n",
" \"type\": \"function\",\n",
" }\n",
" ]\n",
" else:\n",
" tool_calls = None\n",
"\n",
" response_message = {\n",
" \"role\": role,\n",
" \"content\": response_content,\n",
" \"tool_calls\": tool_calls,\n",
" }\n",
" return {\"messages\": [response_message]}"
]
"source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"]
},
{
"cell_type": "markdown",
@@ -187,41 +86,7 @@
"id": "b756ea32",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"\n",
"async def get_items(place: str) -> str:\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
" if \"bed\" in place: # For under the bed\n",
" return \"socks, shoes and dust bunnies\"\n",
" if \"shelf\" in place: # For 'shelf'\n",
" return \"books, penciles and pictures\"\n",
" else: # if the agent decides to ask about a different place\n",
" return \"cat snacks\"\n",
"\n",
"\n",
"# define mapping to look up functions when running tools\n",
"function_name_to_function = {\"get_items\": get_items}\n",
"\n",
"\n",
"async def call_tools(state):\n",
" messages = state[\"messages\"]\n",
"\n",
" tool_call = messages[-1][\"tool_calls\"][0]\n",
" function_name = tool_call[\"function\"][\"name\"]\n",
" function_arguments = tool_call[\"function\"][\"arguments\"]\n",
" arguments = json.loads(function_arguments)\n",
"\n",
" function_response = await function_name_to_function[function_name](**arguments)\n",
" tool_message = {\n",
" \"tool_call_id\": tool_call[\"id\"],\n",
" \"role\": \"tool\",\n",
" \"name\": function_name,\n",
" \"content\": function_response,\n",
" }\n",
" return {\"messages\": [tool_message]}"
]
"source": ["import json\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n if \"bed\" in place: # For under the bed\n return \"socks, shoes and dust bunnies\"\n if \"shelf\" in place: # For 'shelf'\n return \"books, penciles and pictures\"\n else: # if the agent decides to ask about a different place\n return \"cat snacks\"\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"]
},
{
"cell_type": "markdown",
@@ -237,33 +102,7 @@
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Literal\n",
"\n",
"from langgraph.graph import StateGraph, END\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, operator.add]\n",
"\n",
"\n",
"def should_continue(state) -> Literal[\"tools\", END]:\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" if last_message[\"tool_calls\"]:\n",
" return \"tools\"\n",
" return END\n",
"\n",
"\n",
"workflow = StateGraph(State)\n",
"workflow.set_entry_point(\"model\")\n",
"workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n",
"workflow.add_node(\"tools\", call_tools)\n",
"workflow.add_conditional_edges(\"model\", should_continue)\n",
"workflow.add_edge(\"tools\", \"model\")\n",
"graph = workflow.compile()"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"]
},
{
"cell_type": "markdown",
@@ -328,14 +167,7 @@
]
}
],
"source": [
"async for event in graph.astream_events(\n",
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n",
"):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n",
" print(\"LLM token\", event[\"data\"][\"chunk\"].dict())"
]
"source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n print(\"LLM token\", event[\"data\"][\"chunk\"].dict())"]
},
{
"cell_type": "code",
@@ -343,7 +175,7 @@
"id": "adb0f7bc-6e51-478e-bd32-8f72df072d6c",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+648 -682
View File
File diff suppressed because one or more lines are too long
@@ -169,9 +169,7 @@
"from langchain_core.output_parsers import JsonOutputParser\n",
"\n",
"# JSON\n",
"llm = ChatOllama(model=\"llama3.1\", \n",
" format=\"json\", \n",
" temperature=0)\n",
"llm = ChatOllama(model=\"llama3.1\", format=\"json\", temperature=0)\n",
"\n",
"\n",
"prompt = PromptTemplate(\n",
@@ -210,6 +208,7 @@
"from IPython.display import Image, display\n",
"from langgraph.graph import START, END, StateGraph\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
@@ -356,7 +355,7 @@
"workflow.add_node(\"web_search\", web_search) # web search\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(START, retrieve)\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
@@ -381,21 +380,22 @@
"metadata": {},
"outputs": [],
"source": [
"import uuid \n",
"import uuid\n",
"\n",
"\n",
"def predict_custom_agent_answer(example: dict):\n",
" \n",
" config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
" \n",
"\n",
" state_dict = custom_graph.invoke(\n",
" {\"question\": example[\"input\"], \"steps\": []}, config\n",
" )\n",
" \n",
"\n",
" return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n",
"\n",
"\n",
"example = {\"input\": \"What are the types of agent memory?\"}\n",
"#response = predict_custom_agent_answer(example)\n",
"#response"
"# response = predict_custom_agent_answer(example)\n",
"# response"
]
},
{
@@ -544,6 +544,7 @@
" \"generate_answer\",\n",
"]\n",
"\n",
"\n",
"def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
File diff suppressed because one or more lines are too long
@@ -134,6 +134,7 @@
" for d in web_results\n",
" ]\n",
"\n",
"\n",
"# Tool list\n",
"tools = [retrieve_documents, web_search]"
]
@@ -152,9 +153,11 @@
"from langgraph.graph.message import AnyMessage, add_messages\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
"\n",
"\n",
"class Assistant:\n",
" def __init__(self, runnable: Runnable):\n",
" \"\"\"\n",
@@ -291,6 +294,7 @@
"source": [
"import uuid\n",
"\n",
"\n",
"def predict_react_agent_answer(example: dict):\n",
" \"\"\"Use this for answer evaluation\"\"\"\n",
"\n",
+2 -10
View File
@@ -457,13 +457,13 @@
"source": [
"from langchain_core.runnables import RunnableLambda\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.graph import END, START, StateGraph\n",
"\n",
"graph_builder = StateGraph(AgentState)\n",
"\n",
"\n",
"graph_builder.add_node(\"agent\", agent)\n",
"graph_builder.set_entry_point(\"agent\")\n",
"graph_builder.add_edge(START, \"agent\")\n",
"\n",
"graph_builder.add_node(\"update_scratchpad\", update_scratchpad)\n",
"graph_builder.add_edge(\"update_scratchpad\", \"agent\")\n",
@@ -751,14 +751,6 @@
")\n",
"print(f\"Final response: {res}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dcb656d9-fff5-42b0-9b16-60716a51ca0b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+3 -3
View File
@@ -38,11 +38,11 @@ lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests:
poetry run ruff .
poetry run ruff check .
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff:
poetry run ruff format $(PYTHON_FILES)
poetry run ruff --select I --fix $(PYTHON_FILES)
poetry run ruff check --select I --fix $(PYTHON_FILES)
+4
View File
@@ -2,6 +2,10 @@
Implementation of LangGraph CheckpointSaver that uses Postgres.
## Dependencies
By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
## Usage
> [!IMPORTANT]
@@ -1,12 +1,13 @@
import threading
from contextlib import contextmanager
from typing import Any, Iterator, List, Optional
from typing import Any, Iterator, List, Optional, Union
from langchain_core.runnables import RunnableConfig
from psycopg import Connection, Cursor, Pipeline
from psycopg.errors import UndefinedTable
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import (
ChannelVersions,
@@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import (
from langgraph.checkpoint.serde.base import SerializerProtocol
@contextmanager
def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]:
if isinstance(conn, Connection):
yield conn
elif isinstance(conn, ConnectionPool):
with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
class PostgresSaver(BasePostgresSaver):
lock: threading.Lock
def __init__(
self,
conn: Connection,
conn: Union[Connection, ConnectionPool],
pipe: Optional[Pipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
super().__init__(serde=serde)
if isinstance(conn, ConnectionPool) and pipe is not None:
raise ValueError(
"Pipeline should be used only with a single Connection, not ConnectionPool."
)
self.conn = conn
self.pipe = pipe
self.lock = threading.Lock()
@@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver):
already exist and runs database migrations. It MUST be called directly by the user
the first time checkpointer is used.
"""
with self.lock:
with self.conn.cursor(binary=True, row_factory=dict_row) as cur:
try:
version = cur.execute(
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
).fetchone()["v"]
except UndefinedTable:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
if self.pipe:
self.pipe.sync()
with self._cursor() as cur:
try:
version = cur.execute(
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
).fetchone()["v"]
except UndefinedTable:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
if self.pipe:
self.pipe.sync()
def list(
self,
@@ -138,10 +154,11 @@ class PostgresSaver(BasePostgresSaver):
"checkpoint_id": value["checkpoint_id"],
}
},
{
**self._load_checkpoint(value["checkpoint"]),
"channel_values": self._load_blobs(value["channel_values"]),
},
self._load_checkpoint(
value["checkpoint"],
value["channel_values"],
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
@@ -216,10 +233,11 @@ class PostgresSaver(BasePostgresSaver):
"checkpoint_id": value["checkpoint_id"],
}
},
{
**self._load_checkpoint(value["checkpoint"]),
"channel_values": self._load_blobs(value["channel_values"]),
},
self._load_checkpoint(
value["checkpoint"],
value["channel_values"],
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
@@ -333,23 +351,24 @@ class PostgresSaver(BasePostgresSaver):
@contextmanager
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
try:
with self.conn.cursor(binary=True, row_factory=dict_row) as cur:
with _get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
try:
with conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
finally:
if pipeline:
self.pipe.sync()
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
else:
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
finally:
if pipeline:
self.pipe.sync()
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, self.conn.pipeline(), self.conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
with self.lock, self.conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
@@ -1,12 +1,13 @@
import asyncio
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Optional
from typing import Any, AsyncIterator, Iterator, List, Optional, Union
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
from psycopg.errors import UndefinedTable
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import (
ChannelVersions,
@@ -19,19 +20,38 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
@asynccontextmanager
async def _get_connection(
conn: Union[AsyncConnection, AsyncConnectionPool],
) -> AsyncIterator[AsyncConnection]:
if isinstance(conn, AsyncConnection):
yield conn
elif isinstance(conn, AsyncConnectionPool):
async with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
class AsyncPostgresSaver(BasePostgresSaver):
lock: asyncio.Lock
def __init__(
self,
conn: AsyncConnection,
conn: Union[AsyncConnection, AsyncConnectionPool],
pipe: Optional[AsyncPipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
super().__init__(serde=serde)
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
raise ValueError(
"Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
)
self.conn = conn
self.pipe = pipe
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
@classmethod
@asynccontextmanager
@@ -45,7 +65,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
pipeline (bool): whether to use AsyncPipeline
Returns:
PostgresSaver: A new PostgresSaver instance.
AsyncPostgresSaver: A new AsyncPostgresSaver instance.
"""
async with await AsyncConnection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
@@ -63,25 +83,22 @@ class AsyncPostgresSaver(BasePostgresSaver):
already exist and runs database migrations. It MUST be called directly by the user
the first time checkpointer is used.
"""
async with self.lock:
async with self.conn.cursor(binary=True, row_factory=dict_row) as cur:
try:
results = await cur.execute(
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
)
version = (await results.fetchone())["v"]
except UndefinedTable:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
await cur.execute(migration)
await cur.execute(
f"INSERT INTO checkpoint_migrations (v) VALUES ({v})"
)
if self.pipe:
await self.pipe.sync()
async with self._cursor() as cur:
try:
results = await cur.execute(
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
)
version = (await results.fetchone())["v"]
except UndefinedTable:
version = -1
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
if self.pipe:
await self.pipe.sync()
async def alist(
self,
@@ -121,12 +138,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
"checkpoint_id": value["checkpoint_id"],
}
},
{
**self._load_checkpoint(value["checkpoint"]),
"channel_values": await asyncio.to_thread(
self._load_blobs, value["channel_values"]
),
},
await asyncio.to_thread(
self._load_checkpoint,
value["checkpoint"],
value["channel_values"],
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
@@ -180,12 +197,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
"checkpoint_id": value["checkpoint_id"],
}
},
{
**self._load_checkpoint(value["checkpoint"]),
"channel_values": await asyncio.to_thread(
self._load_blobs, value["channel_values"]
),
},
await asyncio.to_thread(
self._load_checkpoint,
value["checkpoint"],
value["channel_values"],
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
@@ -290,25 +307,119 @@ class AsyncPostgresSaver(BasePostgresSaver):
@asynccontextmanager
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
try:
async with self.conn.cursor(binary=True, row_factory=dict_row) as cur:
async with _get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
try:
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
finally:
if pipeline:
await self.pipe.sync()
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
finally:
if pipeline:
await self.pipe.sync()
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, self.conn.pipeline(), self.conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
async with self.lock, self.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
) as cur:
yield cur
def list(
self,
config: Optional[RunnableConfig],
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
This method retrieves a list of checkpoint tuples from the Postgres database based
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
Args:
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit (Optional[int]): Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
"""
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_), self.loop
).result()
except StopAsyncIteration:
break
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the Postgres database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
Args:
config (RunnableConfig): The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
return asyncio.run_coroutine_threadsafe(
self.aget_tuple(config), self.loop
).result()
def put(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save a checkpoint to the database.
This method saves a checkpoint to the Postgres database. The checkpoint is associated
with the provided config and its parent config (if any).
Args:
config (RunnableConfig): The config to associate with the checkpoint.
checkpoint (Checkpoint): The checkpoint to save.
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
new_versions (ChannelVersions): New channel versions as of this write.
Returns:
RunnableConfig: Updated configuration after storing the checkpoint.
"""
return asyncio.run_coroutine_threadsafe(
self.aput(config, checkpoint, metadata, new_versions), self.loop
).result()
def put_writes(
self,
config: RunnableConfig,
writes: List[tuple[str, Any]],
task_id: str,
) -> None:
"""Store intermediate writes linked to a checkpoint.
This method saves intermediate writes associated with a checkpoint to the database.
Args:
config (RunnableConfig): Configuration of the related checkpoint.
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
task_id (str): Identifier for the task creating the writes.
"""
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
).result()
@@ -1,4 +1,3 @@
from base64 import b64decode, b64encode
from hashlib import md5
from typing import Any, List, Optional, Tuple
@@ -13,7 +12,7 @@ from langgraph.checkpoint.base import (
get_checkpoint_id,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import ChannelProtocol
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
MetadataInput = Optional[dict[str, Any]]
@@ -58,7 +57,7 @@ MIGRATIONS = [
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
]
SELECT_SQL = """
SELECT_SQL = f"""
select
thread_id,
checkpoint,
@@ -77,12 +76,20 @@ select
) as channel_values,
(
select
array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob])
array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, 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
and cw.checkpoint_id = checkpoints.checkpoint_id
) as pending_writes
) as pending_writes,
(
select array_agg(array[cw.type::bytea, cw.blob] order by cw.idx)
from checkpoint_writes cw
where cw.thread_id = checkpoints.thread_id
and cw.checkpoint_ns = checkpoints.checkpoint_ns
and cw.checkpoint_id = checkpoints.parent_checkpoint_id
and cw.channel = '{TASKS}'
) as pending_sends
from checkpoints """
UPSERT_CHECKPOINT_BLOBS_SQL = """
@@ -116,25 +123,23 @@ class BasePostgresSaver(BaseCheckpointSaver):
jsonplus_serde = JsonPlusSerializer()
def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
if len(checkpoint["pending_sends"]) == 2 and all(
isinstance(a, str) for a in checkpoint["pending_sends"]
):
type, bs = checkpoint["pending_sends"]
return {
**checkpoint,
"pending_sends": self.serde.loads_typed((type, b64decode(bs))),
}
return checkpoint
def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]:
type, bs = self.serde.dumps_typed(checkpoint["pending_sends"])
def _load_checkpoint(
self,
checkpoint: dict[str, Any],
channel_values: list[tuple[bytes, bytes, bytes]],
pending_sends: list[tuple[bytes, bytes]],
) -> Checkpoint:
return {
**checkpoint,
"pending_sends": (type, b64encode(bs).decode()),
"pending_sends": [
self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends or []
],
"channel_values": self._load_blobs(channel_values),
}
def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]:
return {**checkpoint, "pending_sends": []}
def _load_blobs(
self, blob_values: list[tuple[bytes, bytes, bytes]]
) -> dict[str, Any]:
@@ -255,9 +260,14 @@ class BasePostgresSaver(BaseCheckpointSaver):
if config:
wheres.append("thread_id = %s ")
param_values.append(config["configurable"]["thread_id"])
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
wheres.append("checkpoint_ns = %s")
param_values.append(checkpoint_ns)
checkpoint_ns = config["configurable"].get("checkpoint_ns")
if checkpoint_ns is not None:
wheres.append("checkpoint_ns = %s")
param_values.append(checkpoint_ns)
if checkpoint_id := get_checkpoint_id(config):
wheres.append("checkpoint_id = %s ")
param_values.append(checkpoint_id)
# construct predicate for metadata filter
if filter:
+23 -21
View File
@@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
[[package]]
name = "langgraph-checkpoint"
version = "1.0.1"
version = "1.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -394,6 +394,8 @@ files = [
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
@@ -764,7 +766,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -822,28 +823,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.1.15"
version = "0.6.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
]
[[package]]
@@ -969,4 +971,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "422b6d716b86db072ea3a612287ad20ff5700c18f22d9e9d59cc4e198514519d"
content-hash = "e294b6996aa6c8f671e6aaf65be8b4aba94c18e2f237dd3ee0e1b777849ce8a8"
+6 -5
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "1.0.3"
version = "1.0.6"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,12 +10,13 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^1.0.1"
langgraph-checkpoint = "^1.0.8"
orjson = ">=3.10.1"
psycopg = {extras = ["binary"], version = ">=3.1.19"}
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
[tool.poetry.group.dev.dependencies]
ruff = "^0.1.4"
ruff = "^0.6.2"
codespell = "^2.2.0"
pytest = "^7.2.1"
anyio = "^4.4.0"
@@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1"
pytest-mock = "^3.11.1"
pytest-watch = "^4.2.0"
mypy = "^1.10.0"
psycopg-pool = "^3.2.2"
psycopg = {extras = ["binary"], version = ">=3.0.0"}
langgraph-checkpoint = {path = "../checkpoint", develop = true}
[tool.pytest.ini_options]
+6 -20
View File
@@ -87,29 +87,15 @@ class TestAsyncPostgresSaver:
search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
assert len(search_results_4) == 0
# search by config (defaults to root graph checkpoints)
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = [
c
async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
]
assert len(search_results_5) == 1
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
# search by config and checkpoint_ns
search_results_6 = [
c
async for c in saver.alist(
{
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "inner",
}
}
)
]
assert len(search_results_6) == 1
assert (
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
)
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
+6 -19
View File
@@ -88,27 +88,14 @@ class TestPostgresSaver:
search_results_4 = list(saver.list(None, filter=query_4))
assert len(search_results_4) == 0
# search by config (defaults to root graph checkpoints)
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = list(
saver.list({"configurable": {"thread_id": "thread-2"}})
)
assert len(search_results_5) == 1
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
# search by config and checkpoint_ns
search_results_6 = list(
saver.list(
{
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "inner",
}
}
)
)
assert len(search_results_6) == 1
assert (
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
)
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
+3 -3
View File
@@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests:
poetry run ruff .
poetry run ruff check .
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff:
poetry run ruff format $(PYTHON_FILES)
poetry run ruff --select I --fix $(PYTHON_FILES)
poetry run ruff check --select I --fix $(PYTHON_FILES)
@@ -244,7 +244,7 @@ class SqliteSaver(BaseCheckpointSaver):
}
# find any pending writes
cur.execute(
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
(
str(config["configurable"]["thread_id"]),
checkpoint_ns,
@@ -331,7 +331,7 @@ class SqliteSaver(BaseCheckpointSaver):
metadata,
) in cur:
wcur.execute(
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
(thread_id, checkpoint_ns, checkpoint_id),
)
yield CheckpointTuple(
@@ -1,11 +1,11 @@
import asyncio
import functools
from contextlib import asynccontextmanager
from typing import (
Any,
AsyncIterator,
Dict,
Iterator,
List,
Optional,
Sequence,
Tuple,
@@ -31,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where
T = TypeVar("T", bound=callable)
def not_implemented_sync_method(func: T) -> T:
@functools.wraps(func)
def wrapper(*args, **kwargs):
raise NotImplementedError(
"The AsyncSqliteSaver does not support synchronous methods. "
"Consider using the SqliteSaver instead.\n"
"from langgraph.checkpoint.sqlite import SqliteSaver\n"
"See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver "
"for more information."
)
return wrapper
class AsyncSqliteSaver(BaseCheckpointSaver):
"""An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
@@ -132,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
self.jsonplus_serde = JsonPlusSerializer()
self.conn = conn
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
self.is_setup = False
@classmethod
@@ -150,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
async with aiosqlite.connect(conn_string) as conn:
yield AsyncSqliteSaver(conn)
@not_implemented_sync_method
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database.
Note:
This method is not implemented for the AsyncSqliteSaver. Use `aget` instead.
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
"""
This method retrieves a checkpoint tuple from the SQLite database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
Args:
config (RunnableConfig): The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
return asyncio.run_coroutine_threadsafe(
self.aget_tuple(config), self.loop
).result()
@not_implemented_sync_method
def list(
self,
config: Optional[RunnableConfig],
@@ -168,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
"""List checkpoints from the database asynchronously.
Note:
This method is not implemented for the AsyncSqliteSaver. Use `alist` instead.
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
This method retrieves a list of checkpoint tuples from the SQLite database based
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
Args:
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit (Optional[int]): Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
"""
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
while True:
try:
yield asyncio.run_coroutine_threadsafe(
anext(aiter_), self.loop
).result()
except StopAsyncIteration:
break
@not_implemented_sync_method
def put(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Save a checkpoint to the database. FOO"""
"""Save a checkpoint to the database.
This method saves a checkpoint to the SQLite database. The checkpoint is associated
with the provided config and its parent config (if any).
Args:
config (RunnableConfig): The config to associate with the checkpoint.
checkpoint (Checkpoint): The checkpoint to save.
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
new_versions (ChannelVersions): New channel versions as of this write.
Returns:
RunnableConfig: Updated configuration after storing the checkpoint.
"""
return asyncio.run_coroutine_threadsafe(
self.aput(config, checkpoint, metadata, new_versions), self.loop
).result()
def put_writes(
self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str
) -> None:
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
).result()
async def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
@@ -278,7 +312,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
}
# find any pending writes
await cur.execute(
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
(
str(config["configurable"]["thread_id"]),
checkpoint_ns,
@@ -348,7 +382,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
metadata,
) in cur:
await wcur.execute(
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
(thread_id, checkpoint_ns, checkpoint_id),
)
yield CheckpointTuple(
@@ -70,9 +70,14 @@ def search_where(
if config is not None:
wheres.append("thread_id = ?")
param_values.append(config["configurable"]["thread_id"])
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
wheres.append("checkpoint_ns = ?")
param_values.append(checkpoint_ns)
checkpoint_ns = config["configurable"].get("checkpoint_ns")
if checkpoint_ns is not None:
wheres.append("checkpoint_ns = ?")
param_values.append(checkpoint_ns)
if checkpoint_id := get_checkpoint_id(config):
wheres.append("checkpoint_id = ?")
param_values.append(checkpoint_id)
# construct predicate for metadata filter
if filter:
+23 -21
View File
@@ -252,7 +252,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
[[package]]
name = "langgraph-checkpoint"
version = "1.0.1"
version = "1.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -380,6 +380,8 @@ files = [
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
@@ -649,7 +651,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -707,28 +708,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.1.15"
version = "0.6.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
]
[[package]]
@@ -832,4 +834,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0"
content-hash = "e073e1a73cdae1fae8ea46499c39e55980cb61c7c8bd6be774c64f80a627eb31"
content-hash = "752a22dc2b57a0818a3a4d9bf5f62226ba7f8e0c458892551bf8b4c93723dbc1"
+3 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "1.0.0"
version = "1.0.2"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,11 +10,11 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^1.0.1"
langgraph-checkpoint = "^1.0.8"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
ruff = "^0.1.4"
ruff = "^0.6.2"
codespell = "^2.2.0"
pytest = "^7.2.1"
pytest-asyncio = "^0.21.1"
+6 -20
View File
@@ -84,29 +84,15 @@ class TestAsyncSqliteSaver:
search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
assert len(search_results_4) == 0
# search by config (defaults to root graph checkpoints)
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = [
c
async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
]
assert len(search_results_5) == 1
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
# search by config and checkpoint_ns
search_results_6 = [
c
async for c in saver.alist(
{
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "inner",
}
}
)
]
assert len(search_results_6) == 1
assert (
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
)
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
+6 -19
View File
@@ -87,28 +87,15 @@ class TestSqliteSaver:
search_results_4 = list(saver.list(None, filter=query_4))
assert len(search_results_4) == 0
# search by config (defaults to root graph checkpoints)
# search by config (defaults to checkpoints across all namespaces)
search_results_5 = list(
saver.list({"configurable": {"thread_id": "thread-2"}})
)
assert len(search_results_5) == 1
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
# search by config and checkpoint_ns
search_results_6 = list(
saver.list(
{
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "inner",
}
}
)
)
assert len(search_results_6) == 1
assert (
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
)
assert len(search_results_5) == 2
assert {
search_results_5[0].config["configurable"]["checkpoint_ns"],
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
+3 -3
View File
@@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests:
poetry run ruff .
poetry run ruff check .
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff:
poetry run ruff format $(PYTHON_FILES)
poetry run ruff --select I --fix $(PYTHON_FILES)
poetry run ruff check --select I --fix $(PYTHON_FILES)
@@ -1,4 +1,3 @@
from abc import ABC
from datetime import datetime, timezone
from typing import (
Any,
@@ -52,10 +51,10 @@ class CheckpointMetadata(TypedDict, total=False):
Mapping from node name to writes emitted by that node.
"""
score: Optional[int]
"""The score of the checkpoint.
parents: dict[str, str]
"""The IDs of the parent checkpoints.
The score can be used to mark a checkpoint as "good".
Mapping from checkpoint namespace to checkpoint ID.
"""
@@ -192,7 +191,7 @@ CheckpointId = ConfigurableFieldSpec(
)
class BaseCheckpointSaver(ABC):
class BaseCheckpointSaver:
"""Base class for creating a graph checkpointer.
Checkpointers allow LangGraph agents to persist their state
@@ -17,6 +17,7 @@ from langgraph.checkpoint.base import (
SerializerProtocol,
get_checkpoint_id,
)
from langgraph.checkpoint.serde.types import TASKS
class MemorySaver(
@@ -108,9 +109,22 @@ class MemorySaver(
if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id):
checkpoint, metadata, parent_checkpoint_id = saved
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
else:
sends = []
return CheckpointTuple(
config=config,
checkpoint=self.serde.loads_typed(checkpoint),
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
},
metadata=self.serde.loads_typed(metadata),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
@@ -130,6 +144,16 @@ class MemorySaver(
checkpoint_id = max(checkpoints.keys())
checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id]
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
else:
sends = []
return CheckpointTuple(
config={
"configurable": {
@@ -138,7 +162,10 @@ class MemorySaver(
"checkpoint_id": checkpoint_id,
}
},
checkpoint=self.serde.loads_typed(checkpoint),
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
},
metadata=self.serde.loads_typed(metadata),
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
@@ -177,62 +204,94 @@ class MemorySaver(
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
"""
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
checkpoint_ns = (
config["configurable"].get("checkpoint_ns", "") if config else ""
config_checkpoint_ns = (
config["configurable"].get("checkpoint_ns") if config else None
)
config_checkpoint_id = get_checkpoint_id(config) if config else None
for thread_id in thread_ids:
for checkpoint_id, (checkpoint, metadata_b, parent_checkpoint_id) in sorted(
self.storage[thread_id][checkpoint_ns].items(),
key=lambda x: x[0],
reverse=True,
):
# filter by checkpoint ID
for checkpoint_ns in self.storage[thread_id].keys():
if (
before
and (before_checkpoint_id := get_checkpoint_id(before))
and checkpoint_id >= before_checkpoint_id
config_checkpoint_ns is not None
and checkpoint_ns != config_checkpoint_ns
):
continue
# filter by metadata
metadata = self.serde.loads_typed(metadata_b)
if filter and not all(
query_value == metadata[query_key]
for query_key, query_value in filter.items()
for checkpoint_id, (
checkpoint,
metadata_b,
parent_checkpoint_id,
) in sorted(
self.storage[thread_id][checkpoint_ns].items(),
key=lambda x: x[0],
reverse=True,
):
continue
# filter by checkpoint ID from config
if config_checkpoint_id and checkpoint_id != config_checkpoint_id:
continue
# limit search results
if limit is not None and limit <= 0:
break
elif limit is not None:
limit -= 1
# filter by checkpoint ID from `before` config
if (
before
and (before_checkpoint_id := get_checkpoint_id(before))
and checkpoint_id >= before_checkpoint_id
):
continue
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
# filter by metadata
metadata = self.serde.loads_typed(metadata_b)
if filter and not all(
query_value == metadata.get(query_key)
for query_key, query_value in filter.items()
):
continue
yield CheckpointTuple(
config={
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
# limit search results
if limit is not None and limit <= 0:
break
elif limit is not None:
limit -= 1
writes = self.writes[
(thread_id, checkpoint_ns, checkpoint_id)
].values()
if parent_checkpoint_id:
sends = [
w[2]
for w in self.writes[
(thread_id, checkpoint_ns, parent_checkpoint_id)
].values()
if w[1] == TASKS
]
else:
sends = []
yield CheckpointTuple(
config={
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": checkpoint_id,
}
},
checkpoint={
**self.serde.loads_typed(checkpoint),
"pending_sends": [self.serde.loads_typed(s) for s in sends],
},
metadata=metadata,
parent_config={
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
},
checkpoint=self.serde.loads_typed(checkpoint),
metadata=metadata,
parent_config={
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": parent_checkpoint_id,
}
}
if parent_checkpoint_id
else None,
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
],
)
if parent_checkpoint_id
else None,
pending_writes=[
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
],
)
def put(
self,
@@ -255,12 +314,14 @@ class MemorySaver(
Returns:
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
"""
c = checkpoint.copy()
c.pop("pending_sends")
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
self.storage[thread_id][checkpoint_ns].update(
{
checkpoint["id"]: (
self.serde.dumps_typed(checkpoint),
self.serde.dumps_typed(c),
self.serde.dumps_typed(metadata),
config["configurable"].get("checkpoint_id"), # parent
)
@@ -338,7 +399,14 @@ class MemorySaver(
"""
loop = asyncio.get_running_loop()
iter = await loop.run_in_executor(
None, partial(self.list, before=before, limit=limit, filter=filter), config
None,
partial(
self.list,
before=before,
limit=limit,
filter=filter,
),
config,
)
while True:
# handling StopIteration exception inside coroutine won't work
@@ -12,17 +12,13 @@ class SerializerProtocol(Protocol):
Valid implementations include the `pickle`, `json` and `orjson` modules.
"""
def dumps(self, obj: Any) -> bytes:
...
def dumps(self, obj: Any) -> bytes: ...
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
...
def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ...
def loads(self, data: bytes) -> Any:
...
def loads(self, data: bytes) -> Any: ...
def loads_typed(self, data: tuple[str, bytes]) -> Any:
...
def loads_typed(self, data: tuple[str, bytes]) -> Any: ...
class SerializerCompat(SerializerProtocol):
@@ -7,6 +7,7 @@ import re
from collections import deque
from datetime import date, datetime, time, timedelta, timezone
from enum import Enum
from inspect import isclass
from ipaddress import (
IPv4Address,
IPv4Interface,
@@ -50,9 +51,13 @@ class JsonPlusSerializer(SerializerProtocol):
if isinstance(obj, Serializable):
return obj.to_json()
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump())
return self._encode_constructor_args(
obj.__class__, method=[None, "model_construct"], kwargs=obj.model_dump()
)
elif hasattr(obj, "dict") and callable(obj.dict):
return self._encode_constructor_args(obj.__class__, kwargs=obj.dict())
return self._encode_constructor_args(
obj.__class__, method=[None, "construct"], kwargs=obj.dict()
)
elif isinstance(obj, pathlib.Path):
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
elif isinstance(obj, re.Pattern):
@@ -111,7 +116,7 @@ class JsonPlusSerializer(SerializerProtocol):
obj.__class__, method="fromhex", args=[obj.hex()]
)
elif isinstance(obj, BaseException):
return self._encode_constructor_args(obj.__class__, args=obj.args)
return repr(obj)
else:
raise TypeError(
f"Object of type {obj.__class__.__name__} is not JSON serializable"
@@ -131,19 +136,30 @@ class JsonPlusSerializer(SerializerProtocol):
# Import class
cls = getattr(mod, name)
# Instantiate class
if value["method"] is not None:
method = getattr(cls, value["method"])
if isinstance(value["method"], str):
methods = [getattr(cls, value["method"])]
elif isinstance(value["method"], list):
methods = [
cls if method is None else getattr(cls, method)
for method in value["method"]
]
else:
method = cls
if value["args"] and value["kwargs"]:
return method(*value["args"], **value["kwargs"])
elif value["args"]:
return method(*value["args"])
elif value["kwargs"]:
return method(**value["kwargs"])
else:
return method()
except (ImportError, AttributeError):
methods = [cls]
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if value["args"] and value["kwargs"]:
return method(*value["args"], **value["kwargs"])
elif value["args"]:
return method(*value["args"])
elif value["kwargs"]:
return method(**value["kwargs"])
else:
return method()
except Exception:
continue
except Exception:
return None
return LC_REVIVER(value)
@@ -13,6 +13,7 @@ from langchain_core.runnables import RunnableConfig
from typing_extensions import Self
ERROR = "__error__"
TASKS = "__pregel_tasks"
Value = TypeVar("Value")
Update = TypeVar("Update")
@@ -22,34 +23,26 @@ C = TypeVar("C")
class ChannelProtocol(Protocol[Value, Update, C]):
# Mirrors langgraph.channels.base.BaseChannel
@property
def ValueType(self) -> Any:
...
def ValueType(self) -> Any: ...
@property
def UpdateType(self) -> Any:
...
def UpdateType(self) -> Any: ...
def checkpoint(self) -> Optional[C]:
...
def checkpoint(self) -> Optional[C]: ...
def from_checkpoint(
self, checkpoint: Optional[C], config: RunnableConfig
) -> Generator[Self, None, None]:
...
) -> Generator[Self, None, None]: ...
async def afrom_checkpoint(
self, checkpoint: Optional[C], config: RunnableConfig
) -> AsyncGenerator[Self, None]:
...
) -> AsyncGenerator[Self, None]: ...
def update(self, values: Sequence[Update]) -> bool:
...
def update(self, values: Sequence[Update]) -> bool: ...
def get(self) -> Value:
...
def get(self) -> Value: ...
def consume(self) -> bool:
...
def consume(self) -> bool: ...
@runtime_checkable
@@ -58,11 +51,8 @@ class SendProtocol(Protocol):
node: str
arg: Any
def __hash__(self) -> int:
...
def __hash__(self) -> int: ...
def __repr__(self) -> str:
...
def __repr__(self) -> str: ...
def __eq__(self, value: object) -> bool:
...
def __eq__(self, value: object) -> bool: ...
+23 -20
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -380,6 +380,8 @@ files = [
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
@@ -707,28 +709,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.1.15"
version = "0.6.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
]
[[package]]
@@ -847,4 +850,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "3bee25f1adc1349de4358a88037693cec47f6a2b88b809490f9198e6313239f6"
content-hash = "de79db3dc7701542739b3417e9d3f02c3b41167719603ee4d08b92e23b7443ee"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "1.0.4"
version = "1.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
@@ -13,7 +13,7 @@ python = "^3.9.0,<4.0"
langchain-core = ">=0.2.22,<0.3"
[tool.poetry.group.dev.dependencies]
ruff = "^0.1.4"
ruff = "^0.6.2"
codespell = "^2.2.0"
pytest = "^7.2.1"
pytest-asyncio = "^0.21.1"
+1 -1
View File
@@ -122,7 +122,7 @@ def test_serde_jsonplus() -> None:
assert dumped == (
"json",
b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": [null, "model_construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": [null, "construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
)
assert serde.loads_typed(dumped) == {

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