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
Nuno Campos 15c3105748 cli0.1.51 2024-08-22 15:37:02 -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
Vadym BardaandGitHub 8090ca67c5 checkpoint-postgres: pass row_factory in cursor (#1433) 2024-08-22 17:39:21 -04: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
Nuno CamposandGitHub 7074604204 Try to improve async stack traces for exceptions in tasks (#1442)
* Try to improve async stack traces for exceptions in tasks

* Lint
2024-08-22 21:23:48 +00:00
Nuno CamposandGitHub 0720b931e8 Merge pull request #1441 from langchain-ai/nc/22aug/test-checkpointers
Test all checkpointers everywhere we test 1 of them
2024-08-22 14:14:49 -07:00
vbarda 4935cf52bf lint 2024-08-22 16:52:16 -04:00
Nuno Campos 45e3d1a3f1 Test all checkpointers everywhere we test 1 of them 2024-08-22 13:51:38 -07:00
vbarda acd8acf237 lint 2024-08-22 16:47:12 -04:00
Nuno Campos 3ec419a2f6 lib0.2.12 2024-08-22 12:45:27 -07:00
Nuno Campos 8a00a0026e checkpoint1.0.4 2024-08-22 12:45:20 -07:00
Nuno CamposandGitHub 7e32de9405 Remove current_tasks from checkpoint interface (#1440)
* Remove current_tasks from checkpoint interface

- Not used, now clear that it can be supported with put_writes(SCHEDULE)

* Add comment
2024-08-22 19:44:26 +00:00
Nuno CamposandGitHub 96af4c72ce Merge pull request #1439 from langchain-ai/nc/22aug/rm-unused-when-values
Remove unused when values for Interrupt
2024-08-22 12:38:28 -07: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
Nuno Campos f93512e3b3 Remove unused when values for Interrupt 2024-08-22 12:33:25 -07:00
Nuno CamposandGitHub a261e1a497 Fix semantics of put_writes/list (#1436)
* Fix semantics of put_writes/list

- put_writes(error) should not prevent saving future successful if task is retried successfully
- put_writes(writes) should be a no-op if non-error writes already exist for that task (this prevents tasks executed more than once from modifying writes previously saved / acted on)
- checkpoints should not include channel default values (ie those without a version)
- list() should fetch and return writes for each checkpoint

* Lint

* Rm print

* Fix import

* Lint
2024-08-22 19:04:42 +00:00
38daba5259 Better error messages for invalid update in all channel types (#1437)
* langgraph: support multiple edges for Topic channel annotations

* remove support_multiple_edges

* Better error messages for invalid update in all channel types

---------

Co-authored-by: vbarda <vadym@langchain.dev>
2024-08-22 18:59:29 +00:00
Vadym BardaandGitHub 078f9f7275 docs: fix manage conversation history how-to (#1438) 2024-08-22 14:56:34 -04:00
gbaian10andGitHub 22f5367af7 langgraph: fix add_node input schema error (#1332) 2024-08-22 12:32:21 -04:00
Hassan MemonandGitHub 4e2b508ebb docs: remove extra line from generate function (#1392) 2024-08-22 12:27:23 -04:00
Vadym BardaandGitHub 7ec3c0e827 langgraph: release 0.2.11 (#1432) 2024-08-22 11:54:47 -04:00
Nuno CamposandGitHub 82408bacf1 Merge pull request #1431 from langchain-ai/vb/fix-get-state-bug
langgraph: check for empty checkpoint when creating snapshot tasks
2024-08-22 08:52:00 -07:00
vbarda 9a306ce931 langgraph: check for empty checkpoint when creating snapshot tasks 2024-08-22 11:47:36 -04:00
Harrison ChaseandGitHub a9a59dd4e4 fix notebook (#1422) 2024-08-22 09:29:23 -04:00
Nuno Campos a545a70afb lib0.2.10 2024-08-21 16:33:30 -07:00
Nuno CamposandGitHub 4521f9312d Merge pull request #1420 from langchain-ai/nc/21aug/debug-mode-interrupts-errors
Add interrupts and errors to debug stream mode
2024-08-21 16:31:58 -07:00
Nuno Campos d67419522d Remove error 2024-08-21 16:27:23 -07:00
Nuno Campos d85e267a83 Add interrupts and errors to debug stream mode
- move remaining output code from Pregel.stream to PregelLoop
2024-08-21 16:23:48 -07:00
Vadym BardaandGitHub 426125cf96 Merge branch 'main' into vb/update-get-state 2024-08-21 19:20:20 -04:00
Nuno CamposandGitHub 4827377191 Merge pull request #1419 from langchain-ai/nc/21aug/cli-redis
cli: Add redis container to `langgraph up`
2024-08-21 16:19:09 -07: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
Nuno Campos 37848a5361 cli: Add redis container to langgraph up 2024-08-21 14:37:36 -07:00
Nuno Campos 9147d05cc4 lib0.2.9 2024-08-21 14:32:15 -07:00
Nuno CamposandGitHub 46171dd759 Merge pull request #1418 from langchain-ai/nc/21aug/fix-pending-interrupt-run
Fix pending run when interrupt exception is used
2024-08-21 14:29:59 -07:00
Nuno Campos 47ed3d97e9 Fix pending run when interrupt exception is used 2024-08-21 14:25:00 -07:00
Nuno CamposandGitHub 06c2481783 Merge pull request #1415 from langchain-ai/nc/21aug/loop-rm-pregel-arg
Remove graph arg to PregelLoop
2024-08-21 14:24:40 -07:00
Vadym BardaandGitHub 1907646bd4 sdk-py: release 0.1.28 (#1417) 2024-08-21 17:10:18 -04:00
Vadym BardaandGitHub 3a65f83ae1 sdk-py: allow passing custom headers (#1416) 2024-08-21 21:05:03 +00:00
Nuno CamposandGitHub 57811a6bfd Merge pull request #1414 from langchain-ai/nc/21aug/combine-managers
Combine channel and managed values manager
2024-08-21 13:49:15 -07:00
Nuno Campos 02697b5712 Remove graph arg to PregelLoop 2024-08-21 13:45:42 -07:00
Nuno Campos e76f4cc434 Combine channel and managed values manager 2024-08-21 13:30:18 -07:00
vbarda f51e7ea9a4 pass pending writes in checkpointers 2024-08-21 15:38:30 -04:00
Nuno Campos 14ec51601c lib0.2.8 2024-08-21 12:09:11 -07:00
Nuno CamposandGitHub 14976d4c56 Interrupts shouldn't be retried (#1413)
* Interrupts shouldn't be retried

* Add async test

* Lint
2024-08-21 12:08:58 -07:00
vbarda a94168af7f Merge branch 'main' into vb/update-get-state 2024-08-21 15:07:52 -04:00
Nuno Campos 2f41b2891b lib0.2.7 2024-08-21 11:33:54 -07:00
Nuno CamposandGitHub c857a77dd1 Merge pull request #1352 from langchain-ai/nc/14aug/scoped-value
Add SharedValue
2024-08-21 11:33:18 -07:00
Nuno Campos 5fb2c2c6f8 Lint 2024-08-21 11:27:04 -07:00
Nuno Campos 4250ff92b8 Fix 2024-08-21 11:25:37 -07:00
Nuno Campos 7fd4a9ed30 Lint 2024-08-21 10:48:52 -07:00
Nuno Campos 2106b5e4a6 Lint 2024-08-21 10:48:04 -07:00
Nuno Campos 1af0367b34 Add error message 2024-08-21 09:41:53 -07:00
Nuno Campos f037a2e9cb Update docstring 2024-08-21 09:39:58 -07:00
Nuno Campos aa1a6be160 Rename more 2024-08-21 09:36:21 -07:00
Nuno Campos c9e6ee6da7 Rename 2024-08-21 09:34:12 -07:00
Nuno Campos bd7b9cca21 WIP 2024-08-21 09:30:47 -07:00
Nuno Campos b228fc1a9b Add serde 2024-08-21 09:30:34 -07:00
Nuno Campos c3794f1fd3 Use batched async kv inside loop 2024-08-21 09:30:22 -07:00
Nuno Campos 4e1db854f6 Add async test 2024-08-21 09:30:22 -07:00
Nuno Campos 630d9c79ed Split out async batch to sep file 2024-08-21 09:30:22 -07:00
Nuno Campos 8f8f3849fc Lint 2024-08-21 09:30:22 -07:00
Nuno Campos 656f89e16a Lint 2024-08-21 09:30:22 -07:00
Nuno Campos 9b90a24d94 Lint 2024-08-21 09:30:22 -07:00
Nuno Campos 77d7deb033 Rename 2024-08-21 09:30:22 -07:00
Nuno Campos 7ca37afc74 Fix 2024-08-21 09:30:21 -07:00
Nuno Campos 2bc0e2df42 Fix 2024-08-21 09:30:21 -07:00
Nuno Campos 1dc09dc45f Remove warning on write to managed channel 2024-08-21 09:30:21 -07:00
Nuno Campos 1cc02825ea Add ScopedValue
- state shared between threads
2024-08-21 09:30:21 -07:00
Nuno Campos d1b7a787ee lib0.2.6 2024-08-21 09:22:27 -07:00
Nuno CamposandGitHub f8b053a286 Enable raising Interrupt from inside a node, add list of current interrupts to get_state (#1354)
* Enable raising Interrupt from inside a node, add list of current interrupts to get_state

* Lint

* Allow multiple interrupt values in exception

* Better typings

* Fix some tests

* Fix up

* Lint

* Add test

* Lint

* WIP stay=True

* Fix tests for get_state

* Remove ids

* Fix step count

* 999

* less fun

* Undo

* Update debug interface

* Remove ability to pass multiple values

* Undo
2024-08-21 16:19:15 +00:00
David DuongandGitHub ebf060675d feat(js-sdk): add config and parent_config to thread state schema (#1411)
* feat(js-sdk): add config and parent_config to thread state schema

* Bump to 0.0.5
2024-08-21 17:00:10 +02:00
Nuno Campos 3d33b575d6 0.2.5 2024-08-20 16:59:04 -07:00
Vadym BardaandGitHub 314488b5c5 ci: run full test suite on all changes (#1397) 2024-08-20 08:47:12 -04:00
Nuno CamposandGitHub 6e4dc26890 Merge pull request #1396 from langchain-ai/nc/19aug/fix-args
postgres: Fix args to put_writes
2024-08-19 15:57:44 -07:00
Nuno Campos 9cfb4b01c8 postgres: Fix args to put_writes 2024-08-19 15:54:49 -07:00
Nuno CamposandGitHub e7d2621a05 Merge pull request #1395 from langchain-ai/nc/19aug/put-writes-idempotent
checkpoint-*: In put_writes clear any previously saved writes for this task_id
2024-08-19 15:48:40 -07:00
Nuno Campos c0c534cbe3 Add pipeline arg 2024-08-19 15:44:22 -07:00
Nuno Campos 7aaeedd7ba checkpoint-*: In put_writes clear any previously saved writes for this task_id
- Previously implementations were clearing only tasks if the index matched a previously saved one, which isn't enough to guarantee idempotency
2024-08-19 15:40:04 -07:00
Nuno CamposandGitHub e15d56c2f2 Merge pull request #1390 from langchain-ai/nc/19aug/add-xray-test
Add xray test for issue #593
2024-08-19 12:09:51 -07:00
Nuno Campos 8a7c6b4fa7 Add xray test 2024-08-19 12:05:36 -07:00
Nuno CamposandGitHub 2e073473dc Merge pull request #1389 from langchain-ai/nc/19aug/fix-ready-signal
cli: Fix ready signal
2024-08-19 08:26:14 -07:00
Nuno Campos 31dcd15927 cli: Fix ready signal 2024-08-19 07:52:00 -07: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
Nuno CamposandGitHub fc95028738 Merge pull request #1370 from langchain-ai/nc/16aug/checkpoint-errors
Checkpoint errors in pending_writes, expose under StateSnapshot.tasks
2024-08-16 13:15:40 -07:00
Nuno Campos e916cab08f Checkpoint errors in pending_writes, expose under StateSnapshot.tasks
- Save errors produced by tasks, under pending_writes
- Re-work logic to cancel other tasks when one fails, ready to change for interrupt exception
- Update serializer to handle exceptions
- Update get_state/get_state_history with new return value property "tasks" which contains a richer description of the next tasks, currently with id, name and error (if already ran and errored)
2024-08-16 13:11:27 -07:00
Vadym BardaandGitHub e4f1f90127 update pyproject in monorepo to reflect consistent min python version (#1369) 2024-08-16 15:41:56 +00:00
Nuno Campos a16a33806c lib0.2.4 2024-08-15 15:37:51 -07:00
Nuno CamposandGitHub 007f419bdc Merge pull request #1363 from Glavin001/patch-1
Include exception traceback in run_with_retry logging
2024-08-15 15:37:01 -07:00
Isaac FranciscoandGitHub 5bee9e92b5 [docs]: add studio desktop callout (#1361) 2024-08-15 22:35:04 +00:00
Nuno Campos 6a68581a9f Lint 2024-08-15 15:32:48 -07:00
Nuno Campos 78781b0a90 Use exc_info 2024-08-15 15:23:54 -07:00
Glavin WiechertandGitHub d15c5a46de Include exception traceback in run_with_retry logging 2024-08-15 18:29:05 -03:00
Isaac FranciscoandGitHub ce37fd3a04 [docs]: studio faqs (#1207)
* draft

* spelling

* rephrases

* vadym comments
2024-08-15 11:56:00 -07:00
Isaac FranciscoandGitHub 64636ee978 [docs]: add auth and small changes (#1356)
* wip

* m

* nit
2024-08-15 18:25:53 +00:00
Isaac FranciscoandGitHub 388a9643a5 [docs]: copying threads (#1350)
* wip

* wip

* wip

* vadym comments
2024-08-15 11:19:18 -07:00
Nuno CamposandGitHub c9f4b5aede Merge pull request #1360 from langchain-ai/nc/14auug/debug-checkpoint-full
Add missing attributes to checkpoint event in debug output
2024-08-15 10:26:27 -07:00
Nuno CamposandGitHub 855139d869 Merge pull request #1299 from langchain-ai/dependabot/pip/libs/langgraph/aiohttp-3.10.2
build(deps-dev): bump aiohttp from 3.9.5 to 3.10.2 in /libs/langgraph
2024-08-15 10:20:16 -07:00
Nuno Campos d07cbd34ce checkpoint1.0.3 2024-08-15 10:19:50 -07:00
Nuno Campos c701fef634 Lint 2024-08-15 09:40:53 -07:00
Nuno Campos 2c8fc3c9c6 Add missing attributes to checkpoint event in debug output 2024-08-15 09:30:29 -07:00
vbarda 45054df71a remove include_subgraph_state kwarg 2024-08-14 21:07:56 -04:00
Isaac FranciscoandGitHub 8ad797e261 fix (#1353) 2024-08-14 17:16:00 -07:00
Nuno CamposandGitHub 2dbfc8aca3 Merge pull request #1344 from langchain-ai/nc/14aug/ignore-serialized-values-missing-constructor
checkpoint: Ignore serialized values when constructor no longer available
2024-08-14 14:43:57 -07:00
vbarda 7fa97898aa correctly propagate all subgraph attributes 2024-08-14 16:15:08 -04:00
Isaac FranciscoandGitHub 3b2d5f02e1 [docs]: cloud how to for checking thread status (#1346)
* draft

* nit

* harrison comments

* spelling
2024-08-14 11:56:43 -07:00
Isaac FranciscoandGitHub 34fd833b04 [docs]: purge message graph (#1330)
* purge message graph

* docstrings

* use with dicts
2024-08-14 11:16:38 -07:00
vbarda 6531ec7669 remove inherit checkpointer 2024-08-14 14:14:06 -04:00
Nuno Campos 728b283063 checkpoint: Ignore serialized values when constructor no longer available 2024-08-14 10:59:14 -07:00
Isaac FranciscoandGitHub a3dd43b39b x (#1343) 2024-08-14 16:32:49 +00:00
Isaac FranciscoandGitHub 41a6eb7cf4 [docs]: add CURL commands to some how-tos (#1325) 2024-08-14 12:31:24 -04:00
vbarda 409b915a3f code review 2024-08-14 12:10:07 -04:00
Isaac FranciscoandGitHub c04133b1df [docs]: cloud hil review tool calls (#1334)
* hil review tool calls

* interrupt note
2024-08-14 08:03:15 -07:00
Hassan MemonandGitHub d1450a463e Improved description for chatbot node function (#1339)
Updated the documentation to describe the chatbot node function's return value in a more Pythonic and enthusiastic way. The description now emphasizes that the function returns a dictionary with the updated messages list neatly tucked under the messages key.
2024-08-14 14:12:37 +00:00
Hassan MemonandGitHub 8daeb56194 Fix typo in documentation: corrected 'they agent's' to 'their agent's' (#1338) 2024-08-14 10:08:26 -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
Nuno CamposandGitHub aa1b94166c Merge pull request #1329 from langchain-ai/nc/13aug/config-checkpoint-passthrough
Passthrough checkpoint in config even when current sub-graph didn't choose to use it
2024-08-13 16:49:12 -07:00
Isaac FranciscoandGitHub 307ae4cf66 link fixes (#1336) 2024-08-13 23:11:01 +00: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
gbaian10andGitHub 0a25e865b9 docs: fix persistence_postgres.ipynb (#1328)
* docs: fix DB_URI port
* docs: remove unuse import
2024-08-13 16:06:13 -04:00
Nuno Campos a01c1c2bfa Passthrough checkpoint in config even when current sub-graph didn't choose to use it 2024-08-13 10:27:48 -07: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
Nuno CamposandGitHub e2a72bf511 Merge pull request #1311 from langchain-ai/nc/11aug/serde-more
checkpoint: Add support for serializing more python types
2024-08-12 17:54:07 -07:00
Vadym BardaandGitHub 519b3b9ce1 docs: add async python<=3.10 warning to streaming concepts (#1324)
* docs: add async python<=3.10 warning to streaming concepts
2024-08-13 00:48:20 +00: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
Lenni JustenandGitHub 408676913c Remove extra bullet from index.md (#1149) 2024-08-12 17:21:32 -04:00
Vadym BardaandGitHub 21c842baf6 docs: update streaming example (#1321)
* docs: update streaming example
2024-08-12 21:11:07 +00:00
Vadym BardaandGitHub a03886b9e4 Merge branch 'main' into vb/update-get-state 2024-08-12 16:53:18 -04:00
Lance MartinandGitHub f7bda4c940 Minor clarifications on Memory in docs (#1301)
* Minor clarifications on Memory in docs

* Address comments
2024-08-12 13:42:57 -07: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
gbaian10andGitHub b8233ded23 docs: fix filter_messages error in manage-conversation-history.ipynb (#1315)
fix: #1314
2024-08-12 14:16:59 -04:00
Vadym BardaandGitHub 6a22b7c26f docs: update async connection pool example in postgres how-to (#1318) 2024-08-12 13:24:59 -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
Nuno Campos a64c0a2679 Add pathlib.Path and re.Pattern 2024-08-11 19:40:11 -07:00
Nuno Campos fdc5aece52 Add Decimal 2024-08-11 19:34:22 -07:00
Nuno Campos 685db62029 checkpoint: Add support for serializing more python types
- all ip interfaces
- deque,
- ZoneInfo
- date
- time
2024-08-11 19:27:29 -07:00
Nuno CamposandGitHub 4e643baaa6 Merge pull request #1309 from langchain-ai/nc/11aug/skip-context-get-state
Skip initializing Context channels when calling get_state/get_state_history
2024-08-11 10:56:54 -07:00
Nuno Campos 9634468260 Skip initializing Context channels when calling get_state/get_state_history 2024-08-11 10:47:19 -07:00
Isaac FranciscoandGitHub 00c1b6a2b0 callouts (#1269) 2024-08-09 16:43:03 -07:00
dependabot[bot]andGitHub b45273d112 build(deps-dev): bump aiohttp from 3.9.5 to 3.10.2 in /libs/langgraph
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.5 to 3.10.2.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 18:26:13 +00:00
350adae672 hil concepts (#1298)
* hil concepts

* spelling

---------

Co-authored-by: isaac hershenson <ihershenson@hmc.edu>
2024-08-09 10:59:04 -07:00
Isaac FranciscoandGitHub bdb3832985 [docs]: changing setup docs (#1285)
* wip

* draft
2024-08-09 10:07:39 -07:00
Vadym BardaandGitHub 1fc8718352 docs: update how-to for mongodb (#1297)
* docs: update how-to for mongodb
2024-08-09 16:34:11 +00:00
Vadym BardaandGitHub d98eec6e91 checkpoint-postgres: vbump & add a note to readme (#1295) 2024-08-09 09:42:53 -04:00
Vadym BardaandGitHub 659586947d checkpoint-postgres: fix setup for AsyncPostgresSaver (#1294) 2024-08-09 13:39:30 +00:00
Vadym BardaandGitHub c303ef2a2b docs: update redis how-to (#1286) 2024-08-09 02:06:28 +00:00
ff92beb88a review tool calls (#1283)
* review tool calls

* spelling

* link

* link

---------

Co-authored-by: isaac hershenson <ihershenson@hmc.edu>
2024-08-08 18:02:02 -07:00
Jacob LeeandGitHub dce73fde66 Clarify send API docstring (#1280) 2024-08-08 12:59:23 -07:00
Vadym BardaandGitHub da6462e608 langgraph: release 0.2.3 (#1279) 2024-08-08 12:43:24 -04:00
Vadym BardaandGitHub 49bf8f6816 checkpoint-postgres: release 1.0.2 (#1278) 2024-08-08 12:42:06 -04:00
Vadym BardaandGitHub 71ce07d971 checkpoint: update docstrings for checkpoint libraries (#1277) 2024-08-08 16:26:16 +00:00
Vadym BardaandGitHub a16f86b5bd docs: update postgres persistence example (#1276) 2024-08-08 12:01:19 -04:00
Vadym BardaandGitHub 4dc27b98f1 langgraph, checkpoint-postgres: propagate new versions in update_state (#1270)
* langgraph, checkpoint-postgres: propagate new versions in update_state
2024-08-08 11:55:55 -04:00
Isaac FranciscoandGitHub be8476d981 added context (#1242) 2024-08-08 11:24:52 -04:00
Vadym BardaandGitHub cd92f19858 docs (examples): replace SqliteSaver with MemorySaver (#1271) 2024-08-08 10:30:43 -04:00
a2f4d57bf2 langgraph: more checkpointer tests (#1263)
* langgraph: more checkpointer tests

* more tests

* lint

* update tests

---------

Co-authored-by: Nuno Campos <nuno@langchain.dev>
2024-08-08 00:45:46 +00:00
Nuno CamposandGitHub f9fa35ed82 Merge pull request #1267 from langchain-ai/vb/update-empty-channels
checkpoint-postgres: set unset channel values to empty in blobs
2024-08-07 17:40:58 -07:00
Vadym BardaandGitHub 3b87a81c70 Merge branch 'main' into vb/update-empty-channels 2024-08-07 20:18:10 -04:00
Nuno CamposandGitHub 002b260e7a Various small changes (#1268) 2024-08-07 20:17:47 -04:00
vbarda 659c9881cf cleanup 2024-08-07 19:54:45 -04:00
vbarda b535d38393 test 2024-08-07 19:53:41 -04:00
vbarda d33807b5ea checkpoint-postgres: remove unset channel values from blobs 2024-08-07 17:31:07 -04:00
Nuno Campos 9650f0d41d postgres1.0.1 2024-08-07 12:15:40 -07:00
Nuno Campos 127b6dedee lib0.2.2 2024-08-07 12:14:34 -07:00
Nuno CamposandGitHub db5a6c569a Merge pull request #1265 from langchain-ai/nc/7aug/stream-nodes-wout-writes
Include in stream output all nodes ran, even if they returned no writes to state keys
2024-08-07 12:11:58 -07:00
Nuno CamposandGitHub b47ab6c30a Merge pull request #1264 from langchain-ai/nc/7aug/fix-tests
fix test setup for checkpointers
2024-08-07 12:08:22 -07:00
Nuno Campos 281c97b9e5 Include in stream output all nodes ran, even if they returned no writes to state keys
- This also includes these nodes in checkpoint.metadata.writes
2024-08-07 12:05:45 -07:00
Nuno Campos cb21a3547e Skip async postgres tests in 3.9 2024-08-07 12:04:11 -07:00
Nuno Campos 6028eb02c9 Lint 2024-08-07 11:49:46 -07:00
Nuno Campos 712d52ad3b fix test setup for checkpointers 2024-08-07 11:39:14 -07:00
Nuno CamposandGitHub 6078c408c3 Raise exception for reducers w wrong signature (#1261) 2024-08-07 17:48:07 +00:00
Nuno CamposandGitHub 9caeeaeeec Merge pull request #1258 from langchain-ai/nc/7aug/postgres-dont-sync-too-much
postgres: Avoid syncing pipeline unless actually needed
2024-08-07 09:43:52 -07:00
Nuno Campos 2bd9eb4298 postgres: Avoid syncing pipeline unless actually needed 2024-08-07 09:40:18 -07:00
Nuno CamposandGitHub 4c2542aac6 Merge pull request #1257 from langchain-ai/nc/7aug/postgres-migrations
postgres: Add migration tracking
2024-08-07 09:33:10 -07:00
vbarda 54da68cfe7 remove flag 2024-08-07 12:25:34 -04:00
vbarda 51b62ca0bd remove setup 2024-08-07 12:23:00 -04:00
Vadym BardaandGitHub ca4b65e1a6 checkpoint-postgres: use correct metadata serialization (#1255) 2024-08-07 12:13:44 -04:00
Nuno Campos 1e237bf33a postgres: Add migration tracking 2024-08-07 08:54:08 -07:00
Vadym BardaandGitHub fb8390e138 langgraph: release 0.2.1 (#1250) 2024-08-07 09:55:50 -04:00
Vadym BardaandGitHub 5cab47f751 checkpoint: move memory into a directory to fix namespace import issues (#1249) 2024-08-07 09:41:06 -04:00
Vadym BardaandGitHub 172b4af6ed langgraph: release 0.2.0 (#1246) 2024-08-06 23:25:18 -04:00
Vadym BardaandGitHub 186dfd3976 checkpoint-postgres: release 1.0.0 (#1245) 2024-08-06 22:53:29 -04:00
Vadym BardaandGitHub d9adebed84 checkpoint-sqlite: release 1.0.0 (#1244) 2024-08-06 22:43:35 -04:00
Vadym BardaandGitHub 2b860f04ce checkpoint: release 1.0.1 (#1243) 2024-08-06 22:38:13 -04:00
b37f78942d checkpoint-postgres: new library for postgres checkpointer implementation (#1236)
* checkpoint-postgres: new library for postgres checkpointer implementation

---------

Co-authored-by: Nuno Campos <nuno@langchain.dev>
2024-08-06 22:37:06 -04:00
Nuno CamposandGitHub 81e60893b6 Merge pull request #1240 from langchain-ai/nc/6aug/untracked-value
Add UntrackedValue to mark a state key as not checkpointable
2024-08-06 17:15:19 -07:00
Emil WåreusandGitHub 657cd30045 Update pass_private_state.ipynb (#1213)
Fix typo
2024-08-06 23:59:58 +00:00
Vadym BardaandGitHub 7cf2132df1 Merge branch 'main' into nc/6aug/untracked-value 2024-08-06 19:46:56 -04:00
vbarda b0b6c0ae7b lint + comment 2024-08-06 19:46:39 -04:00
Nuno Campos 5f9b0bc0ae Add UntrackedValue to mark a state key as not checkpointable 2024-08-06 15:41:52 -07:00
Nuno CamposandGitHub f7c09f5e9e Fix output schema affecting stream output (#1239)
* Fix output schema affecting stream output

* Remove file

* Remove file
2024-08-06 22:26:43 +00:00
Andrew NguonlyandGitHub 9c1aeb31dd Remove LangGraph Cloud waitlist from README (#1238)
* Remove waitlist from README.

* Remove waitlist from Cloud index page.

* Remove waitlist from README.
2024-08-06 13:32:55 -07:00
gbaian10andGitHub 43fb6012be fix: the execution error passed from ToolExecutor to create_react_agent (#1234) 2024-08-06 17:51:55 +00:00
Nuno CamposandGitHub 997a2afc79 Merge pull request #1233 from langchain-ai/nc/6aug/add-test-inherited-state-keys
Add test for inherited state keys
2024-08-06 09:30:59 -07:00
Nuno Campos f050f71584 Add test for inherited state keys 2024-08-06 09:20:33 -07:00
Nuno CamposandGitHub 3b63010673 Merge pull request #1232 from langchain-ai/dqbd/js-output-schema
feat(sdk-js): add output schema to match API, expose copy method for thread
2024-08-06 08:18:00 -07:00
Tat Dat Duong bb47a0d9b7 Reformat 2024-08-06 16:24:12 +02:00
Tat Dat Duong 475019731a Bump to 0.0.4 2024-08-06 15:34:28 +02:00
Tat Dat Duong 1e3a0aa88c Expose copy method for thread 2024-08-06 14:15:04 +02:00
Tat Dat Duong 3ba8500e90 feat(sdk-js): Add output schema typedef 2024-08-06 14:06:47 +02:00
Nuno CamposandGitHub 8850e8a373 Merge pull request #1228 from langchain-ai/vb/update-sqlite
checkpoint: stop using sqlite checkpointers as context managers, make memorysaver a context manager
2024-08-05 14:09:42 -07:00
vbarda 566e6c9b60 fix 2024-08-05 17:03:50 -04:00
vbarda 9aaa73cc55 undo comment 2024-08-05 16:55:19 -04:00
vbarda 55717c9bde update memory saver + tests 2024-08-05 16:53:33 -04:00
vbarda 64c30508d9 checkpoint-sqlite: stop using checkpointers as context managers, use contextmanager only in from_conn_string 2024-08-05 16:20:16 -04:00
ccurmeandGitHub a00ace21cb Merge pull request #1227 from langchain-ai/cc/fix_toolbar_title
docs: update title of doc in sidebar
2024-08-05 14:00:29 -04:00
Chester Curme e590a027db update title of doc in sidebar 2024-08-05 13:57:22 -04:00
ccurmeandGitHub 2976d4a3ba Merge pull request #1117 from langchain-ai/cc/many_tools_guide
docs: add how-to guide for handling many tools
2024-08-05 13:55:58 -04:00
Chester Curme 14d448b74d remove usage of upsert 2024-08-05 13:45:47 -04:00
Chester Curme ad31da06d1 update script + index + mkdocs 2024-08-05 13:34:30 -04:00
Chester Curme f124ce2f72 move file 2024-08-05 13:28:54 -04:00
Chester Curme 95f1032215 Merge branch 'main' into cc/many_tools_guide 2024-08-05 13:25:32 -04:00
Nuno CamposandGitHub c2c5b31f4b Merge pull request #1223 from langchain-ai/nc/5aug/test-update-state-custom-class
Add test for update_state when using custom state class
2024-08-05 09:00:34 -07:00
Nuno Campos e8c2fefb29 Add test for update_state when using custom state class 2024-08-05 08:54:16 -07:00
Nuno Campos 4f2f5f7cbc sdk0.1.27 2024-08-03 11:47:21 -07:00
Nuno CamposandGitHub 2ae1121994 sdk: Use an identifiable root path (#1216) 2024-08-03 18:46:36 +00:00
Nuno CamposandGitHub 3b5669a90b Merge pull request #1215 from langchain-ai/nc/3aug/sdk-asgi
sdk: Use ASGI transport when called inside langgraph-api
2024-08-03 11:20:26 -07:00
Nuno Campos 85454b6371 sdk: Use ASGI transport when called inside langgraph-api 2024-08-03 11:17:34 -07:00
Nuno CamposandGitHub 25a98fa888 Merge pull request #1210 from langchain-ai/vb/update-deps
checkpoint-sqlite: add checkpoint dependency
2024-08-02 17:14:54 -07:00
vbarda 2370db0f8c order 2024-08-02 19:08:16 -04:00
vbarda 1b353aed73 checkpoint-sqlite: add checkpoint dependency 2024-08-02 19:06:38 -04:00
Chester Curme 8d4b95afa8 add section 2024-07-26 11:22:01 -04:00
Nuno CamposandGitHub b5f861722d Merge branch 'main' into cc/many_tools_guide 2024-07-25 11:15:05 -07: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
Chester Curme 590f810b53 add concluding text 2024-07-24 12:45:37 -04:00
Chester Curme 7ea5da73c7 add guide 2024-07-24 11:49:05 -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
221 changed files with 35930 additions and 15469 deletions
-9
View File
@@ -24,13 +24,7 @@ jobs:
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
@@ -39,20 +33,17 @@ jobs:
cache-key: core
- name: Install dependencies
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry install --with dev
- name: Run core tests
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
make test
- name: Ensure the tests did not create any additional files
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
+4 -2
View File
@@ -38,7 +38,8 @@
"libs/sdk-py",
"libs/cli",
"libs/checkpoint",
"libs/checkpoint-sqlite"
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres"
]
uses: ./.github/workflows/_lint.yml
with:
@@ -54,7 +55,8 @@
"libs/langgraph",
"libs/cli",
"libs/checkpoint",
"libs/checkpoint-sqlite"
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres"
]
uses: ./.github/workflows/_test.yml
with:
+2 -5
View File
@@ -10,9 +10,6 @@
> [!NOTE]
> Looking for the JS version? Click [here](https://github.com/langchain-ai/langgraphjs) ([JS docs](https://langchain-ai.github.io/langgraphjs/)).
> [!TIP]
> Looking to deploy your LangGraph application? [Join the waitlist](https://www.langchain.com/langgraph-cloud-beta) for [LangGraph Cloud](https://langchain-ai.github.io/langgraph/cloud/), our managed service for deploying and hosting LangGraph applications.
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
@@ -62,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
@@ -110,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(
+6
View File
@@ -38,11 +38,13 @@ _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",
"tool-calling-errors.ipynb",
"pass-config-to-tools.ipynb",
"many-tools.ipynb",
"dynamic-returning-direct.ipynb",
"managing-agent-steps.ipynb",
"respond-in-format.ipynb",
@@ -55,10 +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",
+100 -8
View File
@@ -10,7 +10,7 @@ The LangGraph Cloud API consists of a few core data models: [Assistants](#assist
An assistant is a configured instance of a [`CompiledGraph`][compiledgraph]. It abstracts the cognitive architecture of the graph and contains instance specific configuration and metadata. Multiple assistants can reference the same graph but can contain different configuration and metadata, which may differentiate the behavior of the assistants. An assistant (i.e. the graph) is invoked as part of a run.
The LangGraph Cloud API provides several endpoints for creating and managing assistants. See the <a href="../reference/api/api_ref.html#tag/assistantscreate" target="_blank">API reference</a> for more details.
The LangGraph Cloud API provides several endpoints for creating and managing assistants. See the [API reference](../reference/api/api_ref.html#tag/assistantscreate) for more details.
#### Configuring Assistants
@@ -24,13 +24,13 @@ The state of a thread at a particular point in time is called a checkpoint.
For more on threads and checkpoints, see this section of the [LangGraph conceptual guide](../../concepts/low_level.md#checkpointer).
The LangGraph Cloud API provides several endpoints for creating and managing threads and thread state. See the <a href="../reference/api/api_ref.html#tag/threadscreate" target="_blank">API reference</a> for more details.
The LangGraph Cloud API provides several endpoints for creating and managing threads and thread state. See the [API reference](../reference/api/api_ref.html#tag/threadscreate) for more details.
### Runs
A run is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a thread.
The LangGraph Cloud API provides several endpoints for creating and managing runs. See the <a href="../reference/api/api_ref.html#tag/runscreate" target="_blank">API reference</a> for more details.
The LangGraph Cloud API provides several endpoints for creating and managing runs. See the [API reference](../reference/api/api_ref.html#tag/runscreate) for more details.
### Cron Jobs
@@ -41,7 +41,7 @@ It's often useful to run graphs on some schedule. LangGraph Cloud supports cron
Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs.
The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons" target="_blank">API reference</a> for more details.
The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons) for more details.
## Features
@@ -51,15 +51,107 @@ 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 <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream" target="_blank">API reference</a> for how to create streaming runs.
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
+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()
+101 -50
View File
@@ -1,19 +1,30 @@
# 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.
The final repo structure will look something like this:
```bash
my-app/
|-- requirements.txt # package dependencies
|-- .env # environment variables
|-- openai_agent.py # code for an agent
|-- anthropic_agent.py # code for another agent
|-- langgraph.json # configuration file for LangGraph
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── requirements.txt # package dependencies
│   ├── __init__.py
│   └── agent.py # code for constructing your graph
├── .env # environment variables
└── langgraph.json # configuration file for LangGraph
```
After each step, an example file directory is provided to demonstrate how code can be organized.
@@ -23,31 +34,42 @@ 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.1.19,<0.2.0
langchain-core>=0.2.8,<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>=23.1.0
redis>=5.0.0,<6.0.0
```
Example `requirements.txt` file:
```
langgraph
langchain_anthropic
tavily-python
langchain_community
langchain_openai
```
Example file directory:
```
```bash
my-app/
|-- requirements.txt # Python packages required for your graph
├── my_agent # all project code lies within here
│   └── requirements.txt # package dependencies
```
## Specify Environment Variables
@@ -55,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
@@ -62,42 +85,66 @@ OPENAI_API_KEY=key
```
Example file directory:
```
```bash
my-app/
|-- requirements.txt
|-- .env # file with environment variables
├── my_agent # all project code lies within here
│   └── requirements.txt # package dependencies
└── .env # environment variables
```
## Define Graphs
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each [CompiledGraph][compiledgraph] to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
Example `openai_agent.py` file:
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
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
# my_agent/agent.py
from typing import TypedDict, Literal
model = ChatOpenAI(temperature=0)
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
graph_workflow = MessageGraph()
# Define the config
class GraphConfig(TypedDict):
model_name: Literal["anthropic", "openai"]
graph_workflow.add_node("agent", model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "action",
"end": END,
},
)
workflow.add_edge("action", "agent")
agent = graph_workflow.compile()
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/
|-- requirements.txt
|-- .env
|-- openai_agent.py # code for your graph
|-- anthropic_agent.py # code for your graph
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── requirements.txt # package dependencies
│   ├── __init__.py
│   └── agent.py # code for constructing your graph
└── .env # environment variables
```
## Create LangGraph API Config
@@ -105,33 +152,37 @@ 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": [
"."
],
"graphs": {
"openai_agent": "./openai_agent.py:agent",
"anthropic_agent": "./anthropic_agent.py:agent"
},
"env": "./.env"
"dependencies": ["./my_agent"],
"graphs": {
"agent": "./my_agent/agent.py:graph"
},
"env": ".env"
}
```
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:
```bash
my-app/
|-- requirements.txt
|-- .env
|-- openai_agent.py
|-- anthropic_agent.py
|-- langgraph.json # configuration file for LangGraph
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── requirements.txt # package dependencies
│   ├── __init__.py
│   └── agent.py # code for constructing your graph
├── .env # environment variables
└── langgraph.json # configuration file for LangGraph
```
## Next
+73 -44
View File
@@ -1,16 +1,29 @@
# 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 `pyproject.toml` to define your package's dependencies. If you prefer using `requirements.txt` for dependency management, check out [this how-to guide](./setup.md).
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 `pyproject.toml` to define your package's 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 requirements.txt"
If you prefer using `requirements.txt` for dependency management, check out [this how-to guide](./setup.md).
!!! 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.
The final repo structure will look something like this:
```bash
my-app/
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── __init__.py
│   └── agent.py # code for your graph
│-- .env # environment variables
│-- langgraph.json # configuration file for LangGraph
│   └── agent.py # code for constructing your graph
├── .env # environment variables
├── langgraph.json # configuration file for LangGraph
└── pyproject.toml # dependencies for your project
```
@@ -22,18 +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.1.19,<0.2.0
langchain-core>=0.2.8,<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:
@@ -49,7 +65,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9.0,<3.13"
langgraph = "^0.1.7"
langgraph = "^0.2.0"
langchain-fireworks = "^0.1.3"
@@ -62,9 +78,6 @@ Example file directory:
```bash
my-app/
├── my_agent
│   ├── __init__.py
│   └── agent.py
└── pyproject.toml # Python packages required for your graph
```
@@ -84,10 +97,7 @@ Example file directory:
```bash
my-app/
├── my_agent
│   ├── __init__.py
│   └── agent.py
|-- .env # file with environment variables
├── .env # file with environment variables
└── pyproject.toml
```
@@ -95,26 +105,35 @@ my-app/
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each [CompiledGraph][compiledgraph] to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
Example `agent.py` file:
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-pyproject) to see their implementation):
```python
# my_agent/agent.py
from langchain_fireworks import ChatFireworks
from langgraph.graph import END, StateGraph, add_messages
from typing_extensions import TypedDict, Annotated
from typing import TypedDict, Literal
model = ChatFireworks(model="accounts/fireworks/models/firefunction-v2", temperature=0)
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
class State(TypedDict):
messages: Annotated[list, add_messages]
# Define the config
class GraphConfig(TypedDict):
model_name: Literal["anthropic", "openai"]
graph_workflow = StateGraph(State)
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "action",
"end": END,
},
)
workflow.add_edge("action", "agent")
graph_workflow.add_node("agent", model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
agent = graph_workflow.compile()
graph = workflow.compile()
```
!!! warning "Assign `CompiledGraph` to Variable"
@@ -124,10 +143,15 @@ Example file directory:
```bash
my-app/
├── my_agent
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── __init__.py
│   └── agent.py # code for your graph
|-- .env
│   └── agent.py # code for constructing your graph
├── .env
└── pyproject.toml
```
@@ -141,9 +165,9 @@ Example `langgraph.json` file:
{
"dependencies": ["."],
"graphs": {
"my_fantastic_agent": "./my_agent/agent.py:agent"
"agent": "./my_agent/agent.py:graph"
},
"env": "./.env"
"env": ".env"
}
```
@@ -156,12 +180,17 @@ Example file directory:
```bash
my-app/
├── my_agent
├── my_agent # all project code lies within here
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ └── state.py # state definition of your graph
│   ├── __init__.py
│   └── agent.py # code for your graph
│-- .env
│-- langgraph.json # configuration file for LangGraph
└── pyproject.toml
│   └── agent.py # code for constructing your graph
├── .env # environment variables
├── langgraph.json # configuration file for LangGraph
└── pyproject.toml # dependencies for your project
```
## Next
@@ -38,6 +38,46 @@ Ready!
We can now interact with the API server using the LangGraph SDK. First, we need to start our client, select our assistant (in this case a graph we called "agent", make sure to select the proper assistant you wish to test).
You can either initialize by passing authentication or by setting an environment variable.
#### Initialize with authentication
=== "Python"
```python
from langgraph_sdk import get_client
# only pass the url argument to get_client() if you changed the default port when calling langgraph up
client = get_client(url=<DEPLOYMENT_URL>,api_key=<LANGCHAIN_API_KEY>)
assistant_id = "agent"
thread = await client.threads.create()
```
=== "Javascript"
```js
import { Client } from "@langchain/langgraph-sdk";
// only set the apiUrl if you changed the default port when calling langgraph up
const client = new Client({ apiUrl: <DEPLOYMENT_URL>, apiKey: <LANGCHAIN_API_KEY> });
const assistantId = "agent"
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
--header 'x-api-key: <LANGCHAIN_API_KEY>'
```
#### Initialize with environment variables
If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to explicitly pass authentication to the client
=== "Python"
```python
@@ -60,6 +100,14 @@ We can now interact with the API server using the LangGraph SDK. First, we need
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Now we can invoke our graph to ensure it is working. Make sure to change the input to match the proper schema for your graph.
=== "Python"
@@ -96,4 +144,39 @@ Now we can invoke our graph to ensure it is working. Make sure to change the inp
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]},
\"stream_mode\": [
\"events\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
If your graph works correctly, you should see your graph output displayed in the console. Of course, there are many more ways you might need to test your graph, for a full list of commands you can send with the SDK, see the [Python](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/python_sdk_ref/) and [JS/TS](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/) references.
+65
View File
@@ -0,0 +1,65 @@
# Studio FAQs
## Why is my project failing to start?
There are a few reasons that your project might fail to start, here are some of the most common ones.
### Docker issues
LangGraph Studio requires Docker Desktop version 4.24 or higher. Please make sure you have a version of Docker installed that satisfies that requirement and also make sure you have the Docker Desktop app up and running before trying to use LangGraph Studio. In addition, make sure you have docker-compose updated to version 2.22.0 or higher.
### Configuration or environment issues
Another reason your project might fail to start is because your configuration file is defined incorrectly, or you are missing required environment variables.
## How does interrupt work?
When you select the `Interrupts` dropdown and select a node to interrupt the graph will pause execution before and after (unless the node goes straight to `END`) that node has run. This means that you will be able to both edit the state before the node is ran and the state after the node has ran. This is intended to allow developers more fine-grained control over the behavior of a node and make it easier to observe how the node is behaving. You will not be able to edit the state after the node has ran if the node is the final node in the graph.
## How do I reload the app?
If you would like to reload the app, don't use Command+R as you might normally do. Instead, close and reopen the app for a full refresh.
## How does automatic rebuilding work?
One of the key features of LangGraph Studio is that it automatically rebuilds your image when you change the source code. This allows for a super fast development and testing cycle which makes it easy to iterate on your graph. There are two different ways that LangGraph rebuilds your image: either by editing the image or completely rebuilding it.
### Rebuilds from source code changes
If you modified the source code only (no configuration or dependency changes!) then the image does not require a full rebuild, and LangGraph Studio will only update the relevant parts. The UI status in the bottom left will switch from `Online` to `Stopping` temporarily while the image gets edited. The logs will be shown as this process is happening, and after the image has been edited the status will change back to `Online` and you will be able to run your graph with the modified code!
### Rebuilds from configuration or dependency changes
If you edit your graph configuration file (`langgraph.json`) or the dependencies (either `pyproject.toml` or `requirements.txt`) then the entire image will be rebuilt. This will cause the UI to switch away from the graph view and start showing the logs of the new image building process. This can take a minute or two, and once it is done your updated image will be ready to use!
## Why is my graph taking so long to startup?
The LangGraph Studio interacts with a local LangGraph API server. To stay aligned with ongoing updates, the LangGraph API requires regular rebuilding. As a result, you may occasionally experience slight delays when starting up your project.
## Why are extra edges showing up in my graph?
If you don't define your conditional edges carefully, you might notice extra edges appearing in your graph. This is because without proper definition, LangGraph Studio assumes the conditional edge could access all other nodes. In order for this to not be the case, you need to be explicit about how you define the nodes the conditional edge routes to. There are two ways you can do this:
### Solution 1: Include a path map
The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so:
```python
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
```
In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively.
### Solution 2: Update the typing of the router
Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way:
```python
def routing_function(state: GraphState) -> Literal["node_b","node_c"]:
if state['some_condition'] == True:
return "node_a"
else:
return "node_b"
```
@@ -0,0 +1,200 @@
# Check the Status of your Threads
## Setup
To start, we can setup our client with whatever URL you are hosting your graph from:
### SDK initialization
First, we need to setup our client so that we can communicate with our hosted graph:
=== "Python"
```python
from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
=== "Javascript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = agent;
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
## Find idle threads
We can use the following commands to find threads that are idle, which means that all runs executed on the thread have finished running:
=== "Python"
```python
print(await client.threads.search(status="idle",limit=1))
```
=== "Javascript"
```js
console.log(await client.threads.search({status: "idle",limit:1}));
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/search \
--header 'Content-Type: application/json' \
--data '{"status": "idle", "limit": 1}'
```
Output:
[{'thread_id': 'cacf79bb-4248-4d01-aabc-938dbd60ed2c',
'created_at': '2024-08-14T17:36:38.921660+00:00',
'updated_at': '2024-08-14T17:36:38.921660+00:00',
'metadata': {'graph_id': 'agent'},
'status': 'idle',
'config': {'configurable': {}}}]
## Find interrupted threads
We can use the following commands to find threads that have been interrupted in the middle of a run, which could either mean an error occurred before the run finished or a human-in-the-loop breakpoint was reached and the run is waiting to continue:
=== "Python"
```python
print(await client.threads.search(status="interrupted",limit=1))
```
=== "Javascript"
```js
console.log(await client.threads.search({status: "interrupted",limit:1}));
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/search \
--header 'Content-Type: application/json' \
--data '{"status": "interrupted", "limit": 1}'
```
Output:
[{'thread_id': '0d282b22-bbd5-4d95-9c61-04dcc2e302a5',
'created_at': '2024-08-14T17:41:50.235455+00:00',
'updated_at': '2024-08-14T17:41:50.235455+00:00',
'metadata': {'graph_id': 'agent'},
'status': 'interrupted',
'config': {'configurable': {}}}]
## Find busy threads
We can use the following commands to find threads that are busy, meaning they are currently handling the execution of a run:
=== "Python"
```python
print(await client.threads.search(status="busy",limit=1))
```
=== "Javascript"
```js
console.log(await client.threads.search({status: "busy",limit: 1}));
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/search \
--header 'Content-Type: application/json' \
--data '{"status": "busy", "limit": 1}'
```
Output:
[{'thread_id': '0d282b22-bbd5-4d95-9c61-04dcc2e302a5',
'created_at': '2024-08-14T17:41:50.235455+00:00',
'updated_at': '2024-08-14T17:41:50.235455+00:00',
'metadata': {'graph_id': 'agent'},
'status': 'busy',
'config': {'configurable': {}}}]
## Find specific threads
You may also want to check the status of specific threads, which you can do in a few ways:
### Find by ID
You can use the `get` function to find the status of a specific thread, as long as you have the ID saved
=== "Python"
```python
print((await client.threads.get(<THREAD_ID>))['status'])
```
=== "Javascript"
```js
console.log((await client.threads.get(<THREAD_ID>)).status);
```
=== "CURL"
```bash
curl --request GET \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID> \
--header 'Content-Type: application/json' | jq -r '.status'
```
Output:
'idle'
### Find by metadata
The search endpoint for threads also allows you to filter on metadata, which can be helpful if you use metadata to tag threads in order to keep them organized:
=== "Python"
```python
print((await client.threads.search(metadata={"foo":"bar"},limit=1))[0]['status'])
```
=== "Javascript"
```js
console.log((await client.threads.search({metadata: {"foo":"bar"},limit: 1}))[0].status);
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/search \
--header 'Content-Type: application/json' \
--data '{"metadata": {"foo":"bar"}, "limit": 1}' | jq -r '.[0].status'
```
Output:
'idle'
+132
View File
@@ -0,0 +1,132 @@
# Copying Threads
You may wish to copy (i.e. "fork") an existing thread in order to keep the existing thread's history and create independent runs that do not affect the original thread. This guide shows how you can do that.
## Setup
This code assumes you already have a thread to copy. You can read about what a thread is [here](https://langchain-ai.github.io/langgraph/cloud/concepts/api/#threads) and learn how to stream a run on a thread in [these how-to guides](https://langchain-ai.github.io/langgraph/cloud/how-tos/#streaming).
### SDK initialization
First, we need to setup our client so that we can communicate with our hosted graph:
=== "Python"
```python
from langgraph_sdk import get_client
client = get_client(url="<DEPLOYMENT_URL>")
assistant_id = "agent"
thread = await client.threads.create()
```
=== "Javascript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"<DEPLOYMENT_URL>" });
const assistantId = agent;
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data '{
"metadata": {}
}'
```
## Copying a thread
The code below assumes that a thread you'd like to copy already exists.
Copying a thread will create a new thread with the same history as the existing thread, and then allow you to continue executing runs.
### Create copy
=== "Python"
```python
copied_thread = await client.threads.copy(<THREAD_ID>)
```
=== "Javascript"
```js
let copiedThread = await client.threads.copy(<THREAD_ID>);
```
=== "CURL"
```bash
curl --request POST --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/copy \
--header 'Content-Type: application/json'
```
### Verify copy
We can verify that the history from the prior thread did indeed copy over correctly:
=== "Python"
```python
def remove_thread_id(d):
if 'metadata' in d and 'thread_id' in d['metadata']:
del d['metadata']['thread_id']
return d
original_thread_history = list(map(remove_thread_id,await client.threads.get_history(<THREAD_ID>)))
copied_thread_history = list(map(remove_thread_id,await client.threads.get_history(copied_thread['thread_id'])))
# Compare the two histories
assert original_thread_history == copied_thread_history
# if we made it here the assertion passed!
print("The histories are the same.")
```
=== "Javascript"
```js
function removeThreadId(d) {
if (d.metadata && d.metadata.thread_id) {
delete d.metadata.thread_id;
}
return d;
}
// Assuming `client.threads.getHistory(threadId)` is an async function that returns a list of dicts
async function compareThreadHistories(threadId, copiedThreadId) {
const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId);
const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId);
// Compare the two histories
console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory))
// if we made it here the assertion passed!
console.log("The histories are the same.");
}
// Example usage
compareThreadHistories(<THREAD_ID>, copiedThread.thread_id);
```
=== "CURL"
```bash
if diff <(
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -S 'map(del(.metadata.thread_id))'
) <(
curl --request GET --url <DEPLOYMENT_URL>/threads/<COPIED_THREAD_ID>/history | jq -S 'map(del(.metadata.thread_id))'
) >/dev/null; then
echo "The histories are the same."
else
echo "The histories are different."
fi
```
Output:
The histories are the same.
@@ -31,7 +31,7 @@ Then, let's import our required packages and instantiate our client, assistant,
from langchain_core.messages import convert_to_messages
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -42,7 +42,7 @@ Then, let's import our required packages and instantiate our client, assistant,
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
@@ -21,7 +21,7 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -31,11 +31,19 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent"
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
## Adding a breakpoint
We now want to add a breakpoint in our graph run, which we will do before a tool is called.
@@ -82,6 +90,42 @@ And, now let's compile it with a breakpoint before the tool node:
console.log("\n\n");
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]},
\"interrupt_before\": [\"action\"],
\"stream_mode\": [
\"messages\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
Output:
@@ -17,7 +17,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -27,11 +27,19 @@ First, we need to setup our client so that we can communicate with our hosted gr
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
## Editing state
### Initial invocation
@@ -75,6 +83,42 @@ Now let's invoke our graph, making sure to interrupt before the `action` node.
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"search for weather in SF\"}]},
\"interrupt_before\": [\"action\"],
\"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:
{'agent': {'messages': [{'content': [{'text': "Certainly! I'll search for the current weather in San Francisco for you using the search function. Here's how I'll do that:", 'type': 'text'}, {'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-6dbb0167-f8f6-4e2a-ab68-229b2d1fbb64', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
@@ -129,10 +173,22 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot
await client.threads.updateState(thread['thread_id'], {values:{"messages": lastMessage}});
```
=== "CURL"
```bash
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
jq '.values.messages[-1] | (.tool_calls[0].args = {"query": "current weather in Sidi Frej"})' | \
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
--header 'Content-Type: application/json' \
--data @-
```
Output:
{'configurable': {'thread_id': '88d58d3f-4151-47a9-a8e0-e42fdd3527b8',
'thread_ts': '1ef3274b-a809-6913-8002-91536ce6554d'}}
{'configurable': {'thread_id': '9c8f1a43-9dd8-4017-9271-2c53e57cf66a',
'checkpoint_ns': '',
'checkpoint_id': '1ef58e7e-3641-649f-8002-8b4305a64858'}}
@@ -171,6 +227,40 @@ Now we can resume our graph run but with the updated state:
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"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:
{'action': {'messages': [{'content': '["I looked up: current weather in Sidi Frej. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '1161b8d1-bee4-4188-9be8-698aecb69f10', 'tool_call_id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}]}}
@@ -0,0 +1,575 @@
# Review Tool Calls
Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:
- A tool call to execute SQL, which will then be run by the tool
- A tool call to generate a summary, which will then be saved to the State of the graph
Note that using tool calls is common **whether actually calling tools or not**.
There are typically a few different interactions you may want to do here:
1. Approve the tool call and continue
2. Modify the tool call manually and then continue
3. Give natural language feedback, and then pass that back to the agent instead of continuing
We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state taking one of the three options above
## Setup
We are not going to show the full code for the graph we are hosting, but you can see it [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input.
### SDK initialization
First, we need to setup our client so that we can communicate with our hosted graph:
=== "Python"
```python
from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
=== "Javascript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
## Example with no review
Let's look at an example when no review is required (because no tools are called)
=== "Python"
```python
input = { 'messages':[{ "role":"user", "content":"hi!" }] }
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id,
input=input,
stream_mode="updates",
interrupt_before=["action"],
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const input = {"messages": [{ "role": "human", "content": "hi!"}] }
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "updates",
interruptBefore: ["action"],
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]}
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}, {'content': [{'text': "Hello! Welcome. How can I assist you today? Is there anything specific you'd like to know or any information you're looking for?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d65e07fb-43ff-4d98-ab6b-6316191b9c8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 355, 'output_tokens': 31, 'total_tokens': 386}}]}
If we check the state, we can see that it is finished
=== "Python"
```python
state = await client.threads.get_state(thread["thread_id"])
print(state['next'])
```
=== "Javascript"
```js
const state = await client.threads.getState(thread["thread_id"]);
console.log(state.next);
```
Output:
[]
## Example of approving tool
Let's now look at what it looks like to approve a tool call. Note that we don't need to pass an interrupt to our streaming calls because the graph (defined [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage)) was already compiled with an interrupt before the `human_review_node`.
=== "Python"
```python
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=input,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "values",
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}]}
If we now check, we can see that it is waiting on human review:
=== "Python"
```python
state = await client.threads.get_state(thread["thread_id"])
print(state['next'])
```
=== "Javascript"
```js
const state = await client.threads.getState(thread["thread_id"]);
console.log(state.next);
```
Output:
['human_review_node']
To approve the tool call, we can just continue the thread with no edits. To do this, we just create a new run with no inputs.
=== "Python"
```python
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=None,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: undefined,
streamMode: "values",
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5d5fd0f1-a939-447e-801a-9aaa812322d3', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 464, 'output_tokens': 50, 'total_tokens': 514}}]}
## Edit Tool Call
Let's now say we want to edit the tool call. E.g. change some of the parameters (or even the tool called!) but then execute that tool.
=== "Python"
```python
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=input,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "values",
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6326da9f-6061-4e12-8586-482e32ab4cab', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
To do this, we first need to update the state. We can do this by passing a message in with the **same** id of the message we want to overwrite. This will have the effect of **replacing** that old message. Note that this is only possible because of the **reducer** we are using that replaces messages with the same ID - read more about that [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state).
=== "Python"
```python
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
state = await client.threads.get_state(thread['thread_id'])
print("Current State:")
print(state['values'])
print("\nCurrent Tool Call ID:")
current_content = state['values']['messages'][-1]['content']
current_id = state['values']['messages'][-1]['id']
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
print(tool_call_id)
# We now need to construct a replacement tool call.
# We will change the argument to be `San Francisco, USA`
# Note that we could change any number of arguments or tool names - it just has to be a valid one
new_message = {
"role": "assistant",
"content": current_content,
"tool_calls": [
{
"id": tool_call_id,
"name": "weather_search",
"args": {"city": "San Francisco, USA"}
}
],
# This is important - this needs to be the same as the message you replacing!
# Otherwise, it will show up as a separate message
"id": current_id
}
await client.threads.update_state(
# This is the config which represents this thread
thread['thread_id'],
# This is the updated value we want to push
{"messages": [new_message]},
# We push this update acting as our human_review_node
as_node="human_review_node"
)
print("\nResuming Execution")
# Let's now continue executing from here
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=None,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const state = await client.threads.getState(thread.thread_id);
console.log("Current State:");
console.log(state.values);
console.log("\nCurrent Tool Call ID:");
const lastMessage = state.values.messages[state.values.messages.length - 1];
const currentContent = lastMessage.content;
const currentId = lastMessage.id;
const toolCallId = lastMessage.tool_calls[0].id;
console.log(toolCallId);
// Construct a replacement tool call
const newMessage = {
role: "assistant",
content: currentContent,
tool_calls: [
{
id: toolCallId,
name: "weather_search",
args: { city: "San Francisco, USA" }
}
],
// Ensure the ID is the same as the message you're replacing
id: currentId
};
await client.threads.updateState(
thread.thread_id, // Thread ID
{
values: { "messages": [newMessage] }, // Updated message
asNode: "human_review_node"
} // Acting as human_review_node
);
console.log("\nResuming Execution");
// Continue executing from here
const streamResponseResumed = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: undefined,
streamMode: "values",
interruptBefore: ["action"],
}
);
for await (const chunk of streamResponseResumed) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
Current State:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
Current Tool Call ID:
toolu_01VzagzsUGZsNMwW1wHkcw7h
Resuming Execution
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nBased on the search result, the weather in San Francisco is sunny! It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d90ce97a-39f9-4330-985e-67c5f351a0c5', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 455, 'output_tokens': 52, 'total_tokens': 507}}]}
## Give feedback to a tool call
Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert these feedback as a mock **RESULT** of the tool call.
There are multiple ways to do this:
You could add a new message to the state (representing the "result" of a tool call)
You could add TWO new messages to the state - one representing an "error" from the tool call, other HumanMessage representing the feedback
Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_node` and how it handles different types of messages.
For this example we will just add a single tool call representing the feedback. Let's see this in action!
=== "Python"
```python
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=input,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "values",
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-4911ac27-3d7c-4edf-a3ca-c2908e3922eb', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
To do this, we first need to update the state. We can do this by passing a message in with the same **tool call id** of the tool call we want to respond to. Note that this is a **different*** ID from above
=== "Python"
```python
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
state = await client.threads.get_state(thread['thread_id'])
print("Current State:")
print(state['values'])
print("\nCurrent Tool Call ID:")
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
print(tool_call_id)
# We now need to construct a replacement tool call.
# We will change the argument to be `San Francisco, USA`
# Note that we could change any number of arguments or tool names - it just has to be a valid one
new_message = {
"role": "tool",
# This is our natural language feedback
"content": "User requested changes: pass in the country as well",
"name": "weather_search",
"tool_call_id": tool_call_id
}
await client.threads.update_state(
# This is the config which represents this thread
thread['thread_id'],
# This is the updated value we want to push
{"messages": [new_message]},
# We push this update acting as our human_review_node
as_node="human_review_node"
)
print("\nResuming execution")
# Let's now continue executing from here
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=None,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const state = await client.threads.getState(thread.thread_id);
console.log("Current State:");
console.log(state.values);
console.log("\nCurrent Tool Call ID:");
const lastMessage = state.values.messages[state.values.messages.length - 1];
const toolCallId = lastMessage.tool_calls[0].id;
console.log(toolCallId);
// Construct a replacement tool call
const newMessage = {
role: "tool",
content: "User requested changes: pass in the country as well",
name: "weather_search",
tool_call_id: toolCallId,
};
await client.threads.updateState(
thread.thread_id, // Thread ID
{
values: { "messages": [newMessage] }, // Updated message
asNode: "human_review_node"
} // Acting as human_review_node
);
console.log("\nResuming Execution");
// Continue executing from here
const streamResponseEdited = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: undefined,
streamMode: "values",
interruptBefore: ["action"],
}
);
for await (const chunk of streamResponseEdited) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
Current State:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
Current Tool Call ID:
toolu_01NNw18j57GEGPZvsa9f1wvX
Resuming execution
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}]}
We can see that we now get to another breakpoint - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue
=== "Python"
```python
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=None,
stream_mode="values",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
```
=== "Javascript"
```js
const streamResponseResumed = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: undefined,
streamMode: "values",
}
);
for await (const chunk of streamResponseResumed) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
```
Output:
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}]}
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6a857bb1-f65b-4b86-93d6-c025e003c777', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 557, 'output_tokens': 38, 'total_tokens': 595}}]}
@@ -14,7 +14,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -24,11 +24,20 @@ First, we need to setup our client so that we can communicate with our hosted gr
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = agent;
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data {}
```
## Replay a state
### Initial invocation
@@ -38,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"],
@@ -53,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"],
@@ -69,6 +78,41 @@ Before replaying a state - we need to create states to replay from! In order to
}
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Please search the weather in SF\"}]},
\"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:
@@ -100,23 +144,35 @@ Now let's get our list of states, and invoke from the third state (right before
console.log(stateToReplay['next']);
```
=== "CURL"
```bash
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -r '.[2].next'
```
Output:
['action']
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": {"thread_ts": state_to_replay['checkpoint_id']}}
checkpoint_id=updated_config["checkpoint_id"]
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
@@ -125,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": {"thread_ts": stateToReplay['checkpoint_id']}},
checkpointId: config["checkpoint_id"]
}
);
for await (const chunk of streamResponse) {
@@ -141,6 +199,51 @@ 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 -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:
{'action': {'messages': [{'content': '["I looked up: current weather in San Francisco. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': 'eba650e5-400e-4938-8508-f878dcbcc532', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}
@@ -165,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"
@@ -176,9 +279,26 @@ 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"
```bash
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | \
jq -c '
.[2] as $state_to_replay |
.[2].values.messages[-1].tool_calls[0].args.query = "current weather in SF" |
{
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 @-
```
Now we can rerun our graph with this new config, starting from the `new_state`, which is a branch of our `state_to_replay`:
@@ -191,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": {"thread_ts": new_state['configurable']['thread_ts']}}
checkpoint_id=config['checkpoint_id']
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
@@ -206,7 +326,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
{
input: null,
streamMode: "updates",
config: {"configurable": {"thread_ts": newState['configurable']['thread_ts']}},
checkpointId: config['checkpoint_id'],
}
);
for await (const chunk of streamResponse) {
@@ -216,6 +336,43 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
}
```
=== "CURL"
```bash
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
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:
@@ -24,7 +24,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -34,11 +34,19 @@ First, we need to setup our client so that we can communicate with our hosted gr
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
## Waiting for user input
### Initial invocation
@@ -80,6 +88,42 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
}
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\"}]},
\"interrupt_before\": [\"ask_human\"],
\"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:
@@ -117,11 +161,31 @@ Because we are treating this as a tool call, we will need to update the state as
await client.threads.updateState(thread['thread_id'], {values: {"messages": toolMessage}, asNode:"ask_human"})
```
=== "CURL"
```bash
curl --request GET \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
| jq -r '.values.messages[-1].tool_calls[0].id' \
| sh -c '
TOOL_CALL_ID="$1"
# Construct the JSON payload
JSON_PAYLOAD=$(printf "{\"messages\": [{\"tool_call_id\": \"%s\", \"type\": \"tool\", \"content\": \"san francisco\"}], \"as_node\": \"ask_human\"}" "$TOOL_CALL_ID")
# Send the updated state
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
--header "Content-Type: application/json" \
--data "${JSON_PAYLOAD}"
' _
```
Output:
{'configurable': {'thread_id': '10d0ee61-db47-48fc-a58c-109a1e68cd73',
'thread_ts': '1ef32729-3cc3-6647-8002-14dcb621b46e'}}
{'configurable': {'thread_id': 'a9f322ae-4ed1-41ec-942b-38cb3d342c3a',
'checkpoint_ns': '',
'checkpoint_id': '1ef58e97-a623-63dd-8002-39a9a9b20be3'}}
### Invoking after receiving human input
@@ -133,7 +197,7 @@ We can now tell the agent to continue. We can just pass in None as the input to
```python
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id, # graph_id
assistant_id,
input=None,
stream_mode="updates",
):
@@ -158,6 +222,40 @@ We can now tell the agent to continue. We can just pass in None as the input to
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"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:
{'agent': {'messages': [{'content': [{'text': "Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
+3
View File
@@ -46,6 +46,7 @@ When creating complex graphs, leaving every decision up to the LLM can be danger
- [How to wait for user input](./human_in_the_loop_user_input.md)
- [How to edit graph state](./human_in_the_loop_edit_state.md)
- [How to replay and branch from prior states](./human_in_the_loop_time_travel.md)
- [How to review tool calls](./human_in_the_loop_review_tool_calls.md)
## LangGraph Studio
@@ -72,3 +73,5 @@ Other guides that may prove helpful!
- [How to configure agents](cloud_examples/configuration_cloud.ipynb)
- [How to convert LangGraph calls to LangGraph cloud calls](cloud_examples/langgraph_to_langgraph_cloud.ipynb)
- [How to integrate webhooks](cloud_examples/webhooks.ipynb)
- [How to copy threads](./copy_threads.md)
- [How to check status of your threads](./check_thread_status.md)
@@ -29,7 +29,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
from langchain_core.messages import convert_to_messages
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -39,7 +39,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
@@ -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"])
+2 -2
View File
@@ -28,7 +28,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
from langchain_core.messages import convert_to_messages
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -38,7 +38,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
@@ -30,7 +30,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
from langchain_core.messages import convert_to_messages
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
assistant_id = "agent"
thread = await client.threads.create()
```
@@ -40,7 +40,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
const assistantId = "agent";
const thread = await client.threads.create();
```
File diff suppressed because one or more lines are too long
+130 -16
View File
@@ -1,12 +1,13 @@
# 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"
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
# create thread
thread = await client.threads.create()
print(thread)
@@ -17,12 +18,19 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// create thread
const thread = await client.threads.create();
console.log(thread)
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Output:
@@ -30,7 +38,9 @@ Output:
{'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd',
'created_at': '2024-06-24T22:16:29.301522+00:00',
'updated_at': '2024-06-24T22:16:29.301522+00:00',
'metadata': {}}
'metadata': {},
'status': 'idle',
'config': {}}
@@ -91,6 +101,41 @@ Streaming events produces responses containing an `event` key (in addition to ot
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
\"stream_mode\": [
\"events\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
Output:
Receiving new event of type: metadata...
@@ -258,9 +303,11 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
):
if (
chunk.event == "events" and
chunk.data["event"] == "on_chat_model_stream"
chunk.data["event"] == "on_chat_model_stream" and
len(chunk.data["data"]["chunk"]["content"]) > 0 and
'text' in chunk.data["data"]["chunk"]["content"][0]
):
llm_response += chunk.data["data"]["chunk"]["content"]
llm_response += chunk.data["data"]["chunk"]["content"][0]['text']
print(llm_response)
```
@@ -278,21 +325,88 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
}
);
for await (const chunk of streamResponse) {
if (chunk.event === "events" && chunk.data.event === "on_chat_model_stream") {
llmResponse += chunk.data.data.chunk.content;
if (chunk.event === "events" && chunk.data.event === "on_chat_model_stream" && chunk.data.chunk.content.length > 0 && 'text' in chunk.data.chunk.content[0]) {
llmResponse += chunk.data.data.chunk.content[0].text;
console.log(llmResponse);
}
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
\"stream_mode\": [
\"events\"
]
}" | sed 's/\r$//' | awk '
/^event:/ { event = $2 }
/^data:/ {
json_data = substr($0, index($0, $2))
if (event == "events") {
print json_data
}
}' | jq -r '
select(.event == "on_chat_model_stream") |
.data.chunk.content[] | .text // empty
' | awk '
BEGIN { llm_response="" }
$0 != "" && $0 != "null" {
llm_response = llm_response $0
print llm_response
}'
```
Output:
b
be
beg
begi
begin
begine
beginen
beginend
The
The search
The search results provide
The search results provide the current weather conditions
The search results provide the current weather conditions in San Francisco.
The search results provide the current weather conditions in San Francisco. According
The search results provide the current weather conditions in San Francisco. According to the data,
The search results provide the current weather conditions in San Francisco. According to the data, as
The search results provide the current weather conditions in San Francisco. According to the data, as of 3
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12,
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024,
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C).
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The win
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is bl
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 k
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph).
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70%
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km).
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice sunny day in San
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice sunny day in San Francisco.
+111 -17
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:
@@ -41,7 +45,7 @@ First let's set up our client and thread:
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
# create thread
thread = await client.threads.create()
print(thread)
@@ -52,20 +56,30 @@ First let's set up our client and thread:
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// create thread
const thread = await client.threads.create();
console.log(thread)
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Output:
{'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86',
'created_at': '2024-06-21T15:48:59.808924+00:00',
'updated_at': '2024-06-21T15:48:59.808924+00:00',
'metadata': {}}
'metadata': {},
'status': 'idle',
'config': {}}
Let's also define a helper function for better formatting of the tool calls in messages
Let's also define a helper function for better formatting of the tool calls in messages (for CURL we will define a helper script called `process_stream.sh`)
=== "Python"
@@ -95,6 +109,69 @@ Let's also define a helper function for better formatting of the tool calls in m
}
```
=== "CURL"
```bash
# process_stream.sh
format_tool_calls() {
echo "$1" | jq -r 'map("Tool Call ID: \(.id), Function: \(.name), Arguments: \(.args)") | join("\n")'
}
process_data_item() {
local data_item="$1"
if echo "$data_item" | jq -e '.role == "user"' > /dev/null; then
echo "Human: $(echo "$data_item" | jq -r '.content')"
else
local tool_calls=$(echo "$data_item" | jq -r '.tool_calls // []')
local invalid_tool_calls=$(echo "$data_item" | jq -r '.invalid_tool_calls // []')
local content=$(echo "$data_item" | jq -r '.content // ""')
local response_metadata=$(echo "$data_item" | jq -r '.response_metadata // {}')
if [ -n "$content" ] && [ "$content" != "null" ]; then
echo "AI: $content"
fi
if [ "$tool_calls" != "[]" ]; then
echo "Tool Calls:"
format_tool_calls "$tool_calls"
fi
if [ "$invalid_tool_calls" != "[]" ]; then
echo "Invalid Tool Calls:"
format_tool_calls "$invalid_tool_calls"
fi
if [ "$response_metadata" != "{}" ]; then
local finish_reason=$(echo "$response_metadata" | jq -r '.finish_reason // "N/A"')
echo "Response Metadata: Finish Reason - $finish_reason"
fi
fi
}
while IFS=': ' read -r key value; do
case "$key" in
event)
event="$value"
;;
data)
if [ "$event" = "metadata" ]; then
run_id=$(echo "$value" | jq -r '.run_id')
echo "Metadata: Run ID - $run_id"
echo "------------------------------------------------"
elif [ "$event" = "messages/partial" ]; then
echo "$value" | jq -c '.[]' | while read -r data_item; do
process_data_item "$data_item"
done
echo "------------------------------------------------"
fi
;;
esac
done
```
Now we can stream by messages, which will return complete messages (at the end of node execution) as well as tokens for any messages generated inside a node:
=== "Python"
@@ -201,6 +278,23 @@ Now we can stream by messages, which will return complete messages (at the end o
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"config\":{\"configurable\":{\"model_name\":\"openai\"}},
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
\"stream_mode\": [
\"messages\"
]
}" | sed 's/\r$//' | ./process_stream.sh
```
Output:
Metadata: Run ID - 1ef2fe5c-6a1d-6575-bc09-d7832711c17e
+49 -3
View File
@@ -9,7 +9,7 @@ First let's set up our client and thread:
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
# create thread
thread = await client.threads.create()
print(thread)
@@ -20,19 +20,28 @@ First let's set up our client and thread:
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// create thread
const thread = await client.threads.create();
console.log(thread)
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Output:
{'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4',
'created_at': '2024-06-24T21:30:07.980789+00:00',
'updated_at': '2024-06-24T21:30:07.980789+00:00',
'metadata': {}}
'metadata': {},
'status': 'idle',
'config': {}}
When configuring multiple streaming modes for a run, responses for each respective mode will be produced. In the following example, note that a `list` of modes (`messages`, `events`, `debug`) is passed to the `stream_mode` parameter and the response contains `events`, `debug`, `messages/complete`, `messages/metadata`, and `messages/partial` event types.
@@ -90,6 +99,43 @@ When configuring multiple streaming modes for a run, responses for each respecti
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in SF?\"}]},
\"stream_mode\": [
\"messages\",
\"events\",
\"debug\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
Output:
Receiving new event of type: metadata...
+48 -11
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:
@@ -16,7 +9,7 @@ First let's set up our client and thread:
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
# create thread
thread = await client.threads.create()
print(thread)
@@ -27,19 +20,28 @@ First let's set up our client and thread:
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// create thread
const thread = await client.threads.create();
console.log(thread)
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Output:
{'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16',
'created_at': '2024-06-21T15:22:07.453100+00:00',
'updated_at': '2024-06-21T15:22:07.453100+00:00',
'metadata': {}}
'metadata': {},
'status': 'idle',
'config': {}}
Now we can stream by updates, which outputs updates made to the state by each node after it has executed:
@@ -93,6 +95,41 @@ Now we can stream by updates, which outputs updates made to the state by each no
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in la\"}]},
\"stream_mode\": [
\"updates\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
Output:
Receiving new event of type: metadata...
+77 -12
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:
@@ -16,7 +9,7 @@ First let's set up our client and thread:
```python
from langgraph_sdk import get_client
client = get_client(url="whatever-your-deployment-url-is")
client = get_client(url=<DEPLOYMENT_URL>)
# create thread
thread = await client.threads.create()
print(thread)
@@ -27,18 +20,28 @@ First let's set up our client and thread:
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "whatever-your-deployment-url-is" });
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// create thread
const thread = await client.threads.create();
console.log(thread)
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
```
Output:
{'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4',
'created_at': '2024-06-24T21:30:07.980789+00:00',
'updated_at': '2024-06-24T21:30:07.980789+00:00',
'metadata': {}}
'metadata': {},
'status': 'idle',
'config': {}}
Now we can stream by values, which streams the full state of the graph after each node has finished executing:
@@ -60,7 +63,6 @@ Now we can stream by values, which streams the full state of the graph after eac
```
=== "Javascript"
```js
const input = {"messages": [{"role": "human", "content": "what's the weather in la"}]}
@@ -80,6 +82,41 @@ Now we can stream by values, which streams the full state of the graph after eac
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]},
\"stream_mode\": [
\"values\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "") {
print data_content "\n"
}
sub(/^event: /, "Receiving event of type: ", $0)
printf "%s...\n", $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content "\n"
}
}
'
```
Output:
@@ -149,6 +186,34 @@ If we want to just get the final result, we can use this endpoint and just keep
}
```
=== "CURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]},
\"stream_mode\": [
\"values\"
]
}" | \
sed 's/\r$//' | \
awk '
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "") {
print data_content
}
}
'
```
Output:
{'messages': [{'content': 'what's the weather in la',
Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

+2 -3
View File
@@ -6,9 +6,6 @@
- We are actively contributing improvements back to LangGraph informed by our work on LangGraph Cloud.
- You can always deploy LangGraph applications on your own infrastructure using the open-source LangGraph project.
!!! danger "Important"
LangGraph Cloud is a closed source, paid product in an invite-only stage. We are currently focused on providing high bandwidth support to make our select early customers successful. If you are interested in applying for access, please fill out [this form](https://www.langchain.com/langgraph-cloud-beta).
!!! warning "Under Construction"
LangGraph Cloud documentation is under construction. Contents may change until general availability.
@@ -26,6 +23,8 @@ The LangGraph Cloud API exposes functionality of your LangGraph application thro
LangGraph Cloud is seamlessly integrated with [LangSmith](https://www.langchain.com/langsmith) and is accessible from within the LangSmith UI.
LangGraph Cloud applications can be tested and debugged using the [LangGraph Studio Desktop](https://github.com/langchain-ai/langgraph-studio).
## Key Features
The LangGraph Cloud API supports key LangGraph features in addition to new functionality for enabling complex, agentic workflows.
+10
View File
@@ -66,6 +66,16 @@ Now that we have set everything up on our local file system, we are ready to hos
## Test the graph build locally
### Using LangGraph Studio Desktop (recommended)
![LangGraph Studio Desktop](./img/graph_video_poster.png)
Testing your graph locally is easy with LangGraph Studio Desktop. LangGraph Studio offers a new way to develop LLM applications by providing a specialized agent IDE that enables visualization, interaction, and debugging of complex agentic applications
With visual graphs and the ability to edit state, you can better understand agent workflows and iterate faster. LangGraph Studio integrates with [LangSmith](https://smith.langchain.com) so you can collaborate with teammates to debug failure modes.
### Using the LangGraph CLI
Before deploying to the cloud, we probably want to test the building of our graph locally. This is useful to make sure we have configured our [CLI configuration file][langgraph.json] correctly and our graph runs.
In order to do this we can first install the LangGraph CLI
+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
}'
```
+19
View File
@@ -56,6 +56,25 @@ This is a pretty advanced interaction pattern. In this interaction pattern, the
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for how to do this in LangGraph.
## Review Tool Calls
This is a specific type of human-in-the-loop interaction but it's worth calling out because it is so common. A lot of agent decisions are made via tool calling, so having a clear UX for reviewing tool calls is handy.
A tool call consists of:
- The name of the tool to call
- Arguments to pass to the tool
Note that these tool calls can obviously be used for actually calling functions, but they can also be used for other purposes, like to route the agent in a specific direction.
You will want to review the tool call for both of these use cases.
When reviewing tool calls, there are few actions you may want to take.
1. Approve the tool call (and let the agent continue on its way)
2. Manually change the tool call, either the tool name or the tool arguments (and let the agent continue on its way after that)
3. Leave feedback on the tool call. This differs from (2) in that you are not changing the tool call directly, but rather leaving natural language feedback suggesting the LLM call it differently (or call a different tool). You could do this by either adding a `ToolMessage` and having the feedback be the result of the tool call, or by adding a `ToolMessage` (that simulates an error) and then a `HumanMessage` (with the feedback).
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for how to do this in LangGraph.
## Map-Reduce
A common pattern in agents is to generate a list of objects, do some work on each of those objects, and then combine the results. This is very similar to the common [map-reduce](https://en.wikipedia.org/wiki/MapReduce) operation. This can be tricky for a few reasons. First, it can be tough to define a structured graph ahead of time because the length of the list of objects may be unknown. Second, in order to do this map-reduce you need multiple versions of the state to exist... but the graph shares a common shared state, so how can this be?
+214 -4
View File
@@ -52,7 +52,11 @@ By default, all nodes in the graph will share the same state. This means that th
### Reducers
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better.
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. There are a few different types of reducers, starting with the default type of reducer:
#### Default Reducer
These two examples show how to use the default reducer:
**Example A:**
@@ -79,6 +83,10 @@ class State(TypedDict):
In this example, we've used the `Annotated` type to specify a reducer function (`operator.add`) for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["hi", "bye"]}`. Notice here that the `bar` key is updated by adding the two lists together.
#### Context Reducer
You can use `Context` channels to define shared resources (such as database connections) that are managed outside of your graph's nodes and excluded from checkpointing. The context manager provided to the Context channel is entered before the first step of the graph execution and exited after the last step, allowing you to set up and clean up resources for the duration of the graph invocation. Read this [how to](https://langchain-ai.github.io/langgraph/how-tos/state-context-key) to see an example of using the `Context` channel in your graph.
### Working with Messages in Graph State
#### Why use messages?
@@ -393,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
@@ -419,10 +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).
#### 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.
+6 -2
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
@@ -25,7 +25,7 @@ LangGraph makes it easy to persist state across graph runs. The guide below show
- [How to manage conversation history](memory/manage-conversation-history.ipynb)
- [How to delete messages](memory/delete-messages.ipynb)
- [How to add summary conversation memory](memory/add-summary-conversation-history.ipynb)
- [How to create a custom checkpointer using Postgres](persistence_postgres.ipynb)
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
- [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
@@ -35,9 +35,11 @@ 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)
- [Review tool calls](human_in_the_loop/review-tool-calls.ipynb)
## Streaming
@@ -60,6 +62,7 @@ These guides show how to use different streaming modes.
- [How to handle tool calling errors](tool-calling-errors.ipynb)
- [How to pass graph state to tools](pass-run-time-values-to-tools.ipynb)
- [How to pass config to tools](pass-config-to-tools.ipynb)
- [How to handle large numbers of tools](many-tools.ipynb)
## State Management
@@ -76,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
+11
View File
@@ -40,4 +40,15 @@ LangGraph also natively provides the following checkpoint implementations.
### SqliteSaver
::: langgraph.checkpoint.sqlite.SqliteSaver
### AsyncPostgresSaver
::: langgraph.checkpoint.postgres.aio.AsyncPostgresSaver
### PostgresSaver
::: langgraph.checkpoint.postgres.PostgresSaver
handler: python
handler: python
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

-1
View File
@@ -65,4 +65,3 @@ Learn from example implementations of graphs designed for specific scenarios and
- [Web Navigation](web-navigation/web_voyager.ipynb): Build an agent that can navigate and interact with websites
- [Competitive Programming](usaco/usaco.ipynb): Build an agent with few-shot "episodic memory" and human-in-the-loop collaboration to solve problems from the USA Computing Olympiad; adapted from the ["Can Language Models Solve Olympiad Programming?"](https://arxiv.org/abs/2404.10952v1) paper by Shi, Tang, Narasimhan, and Yao.
- [Complex data extraction](extraction/retries.ipynb): Build an agent that can use function calling to do complex extraction tasks
-
+12 -2
View File
@@ -129,19 +129,22 @@ 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
- Delete messages: how-tos/memory/delete-messages.ipynb
- Add summary of the conversation history: how-tos/memory/add-summary-conversation-history.ipynb
- Create custom checkpointer using Postgres: how-tos/persistence_postgres.ipynb
- Use Postgres checkpointer for persistence: how-tos/persistence_postgres.ipynb
- Create custom checkpointer using MongoDB: how-tos/persistence_mongodb.ipynb
- 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
- Review tool calls: how-tos/human_in_the_loop/review-tool-calls.ipynb
- Streaming:
- Stream full state: how-tos/stream-values.ipynb
- Stream state updates: how-tos/stream-updates.ipynb
@@ -157,6 +160,7 @@ nav:
- Handle tool calling errors: how-tos/tool-calling-errors.ipynb
- Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
- Handle many tools: how-tos/many-tools.ipynb
- State Management:
- Use Pydantic model as state: how-tos/state-model.ipynb
- Use a context object in state: how-tos/state-context-key.ipynb
@@ -167,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
@@ -214,6 +219,7 @@ nav:
- Wait for User Input: "cloud/how-tos/human_in_the_loop_user_input.md"
- Edit Graph State: "cloud/how-tos/human_in_the_loop_edit_state.md"
- Replay and Branch from Prior States: "cloud/how-tos/human_in_the_loop_time_travel.md"
- Review Tool Calls: "cloud/how-tos/human_in_the_loop_review_tool_calls.md"
- LangGraph Studio:
- Test Cloud Deployment: "cloud/how-tos/test_deployment.md"
- Test Local Deployment: "cloud/how-tos/test_local_deployment.md"
@@ -228,6 +234,8 @@ nav:
- Configure Agents: "cloud/how-tos/cloud_examples/configuration_cloud.ipynb"
- Convert LangGraph calls to LangGraph Cloud calls: "cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb"
- Integrate Webhooks: 'cloud/how-tos/cloud_examples/webhooks.ipynb'
- Copy Threads: 'cloud/how-tos/copy_threads.md'
- Check Status of Threads: "cloud/how-tos/check_thread_status.md"
- Conceptual Guides:
- API Concepts: "cloud/concepts/api.md"
- Cloud Concepts: "cloud/concepts/cloud.md"
@@ -238,6 +246,8 @@ nav:
- JS/TS: "cloud/reference/sdk/js_ts_sdk_ref.md"
- CLI: "cloud/reference/cli.md"
- Environment Variables: "cloud/reference/env_var.md"
- FAQ:
- Studio: "cloud/faq/studio.md"
markdown_extensions:
- abbr
@@ -26,7 +26,10 @@
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
"metadata": {},
"outputs": [],
"source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai"]
"source": [
"# %%capture --no-stderr\n",
"# %pip install -U langgraph langchain langchain_openai"
]
},
{
"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\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""]
"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",
"\n",
"# Optional, add tracing in LangSmith.\n",
"# This will help you visualize and debug the control flow\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""
]
},
{
"cell_type": "markdown",
@@ -55,7 +75,24 @@
"id": "828479af-cf9c-4888-a365-599643a96b55",
"metadata": {},
"outputs": [],
"source": ["from typing import List\n\nimport openai\n\n\n# This is flexible, but you can define your agent here, or call your agent API here.\ndef my_chat_bot(messages: List[dict]) -> dict:\n system_message = {\n \"role\": \"system\",\n \"content\": \"You are a customer support agent for an airline.\",\n }\n messages = [system_message] + messages\n completion = openai.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\"\n )\n return completion.choices[0].message.model_dump()"]
"source": [
"from typing import List\n",
"\n",
"import openai\n",
"\n",
"\n",
"# This is flexible, but you can define your agent here, or call your agent API here.\n",
"def my_chat_bot(messages: List[dict]) -> dict:\n",
" system_message = {\n",
" \"role\": \"system\",\n",
" \"content\": \"You are a customer support agent for an airline.\",\n",
" }\n",
" messages = [system_message] + messages\n",
" completion = openai.chat.completions.create(\n",
" messages=messages, model=\"gpt-3.5-turbo\"\n",
" )\n",
" return completion.choices[0].message.model_dump()"
]
},
{
"cell_type": "code",
@@ -77,7 +114,9 @@
"output_type": "execute_result"
}
],
"source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"]
"source": [
"my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"
]
},
{
"cell_type": "markdown",
@@ -96,7 +135,33 @@
"id": "32c147df-7f90-4b0d-9a6b-671677020353",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nsystem_prompt_template = \"\"\"You are a customer of an airline company. \\\nYou are interacting with a user who is a customer support person. \\\n\n{instructions}\n\nWhen you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt_template),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\ninstructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\nYou want them to give you ALL the money back. \\\nThis trip happened 5 years ago.\"\"\"\n\nprompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n\nmodel = ChatOpenAI()\n\nsimulated_user = prompt | model"]
"source": [
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"system_prompt_template = \"\"\"You are a customer of an airline company. \\\n",
"You are interacting with a user who is a customer support person. \\\n",
"\n",
"{instructions}\n",
"\n",
"When you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt_template),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"instructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\n",
"You want them to give you ALL the money back. \\\n",
"This trip happened 5 years ago.\"\"\"\n",
"\n",
"prompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n",
"\n",
"model = ChatOpenAI()\n",
"\n",
"simulated_user = prompt | model"
]
},
{
"cell_type": "code",
@@ -115,7 +180,12 @@
"output_type": "execute_result"
}
],
"source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"]
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n",
"simulated_user.invoke({\"messages\": messages})"
]
},
{
"cell_type": "markdown",
@@ -153,7 +223,20 @@
"id": "69e2a3a3-40f3-4223-9136-113738440be9",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.adapters.openai import convert_message_to_dict\nfrom langchain_core.messages import AIMessage\n\n\ndef chat_bot_node(messages):\n # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n messages = [convert_message_to_dict(m) for m in messages]\n # Call the chat bot\n chat_bot_response = my_chat_bot(messages)\n # Respond with an AI Message\n return AIMessage(content=chat_bot_response[\"content\"])"]
"source": [
"from langchain_community.adapters.openai import convert_message_to_dict\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"def chat_bot_node(state):\n",
" messages = state[\"messages\"]\n",
" # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n",
" messages = [convert_message_to_dict(m) for m in messages]\n",
" # 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\"])]}"
]
},
{
"cell_type": "markdown",
@@ -169,7 +252,26 @@
"id": "7cad7527-ffa5-4c30-8585-b54a7a18bd98",
"metadata": {},
"outputs": [],
"source": ["def _swap_roles(messages):\n new_messages = []\n for m in messages:\n if isinstance(m, AIMessage):\n new_messages.append(HumanMessage(content=m.content))\n else:\n new_messages.append(AIMessage(content=m.content))\n return new_messages\n\n\ndef simulated_user_node(messages):\n # Swap roles of messages\n new_messages = _swap_roles(messages)\n # 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 HumanMessage(content=response.content)"]
"source": [
"def _swap_roles(messages):\n",
" new_messages = []\n",
" for m in messages:\n",
" if isinstance(m, AIMessage):\n",
" new_messages.append(HumanMessage(content=m.content))\n",
" else:\n",
" new_messages.append(AIMessage(content=m.content))\n",
" return new_messages\n",
"\n",
"\n",
"def simulated_user_node(state):\n",
" messages = state[\"messages\"]\n",
" # Swap roles of messages\n",
" new_messages = _swap_roles(messages)\n",
" # 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)]}"
]
},
{
"cell_type": "markdown",
@@ -192,7 +294,16 @@
"id": "28004fbf-a2f3-46b7-bde7-46c7adaf97fb",
"metadata": {},
"outputs": [],
"source": ["def should_continue(messages):\n if len(messages) > 6:\n return \"end\"\n elif messages[-1].content == \"FINISHED\":\n return \"end\"\n else:\n return \"continue\""]
"source": [
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" if len(messages) > 6:\n",
" return \"end\"\n",
" elif messages[-1].content == \"FINISHED\":\n",
" return \"end\"\n",
" else:\n",
" return \"continue\""
]
},
{
"cell_type": "markdown",
@@ -210,7 +321,37 @@
"id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\ngraph_builder.add_node(\"user\", simulated_user_node)\ngraph_builder.add_node(\"chat_bot\", chat_bot_node)\n# Every response from your chat bot will automatically go to the\n# simulated user\ngraph_builder.add_edge(\"chat_bot\", \"user\")\ngraph_builder.add_conditional_edges(\n \"user\",\n should_continue,\n # If the finish criteria are met, we will stop the simulation,\n # otherwise, the virtual user's message will be sent to your chat bot\n {\n \"end\": END,\n \"continue\": \"chat_bot\",\n },\n)\n# The input will first go to your chat bot\ngraph_builder.add_edge(START, \"chat_bot\")\nsimulation = graph_builder.compile()"]
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"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",
"graph_builder = StateGraph(State)\n",
"graph_builder.add_node(\"user\", simulated_user_node)\n",
"graph_builder.add_node(\"chat_bot\", chat_bot_node)\n",
"# Every response from your chat bot will automatically go to the\n",
"# simulated user\n",
"graph_builder.add_edge(\"chat_bot\", \"user\")\n",
"graph_builder.add_conditional_edges(\n",
" \"user\",\n",
" should_continue,\n",
" # If the finish criteria are met, we will stop the simulation,\n",
" # otherwise, the virtual user's message will be sent to your chat bot\n",
" {\n",
" \"end\": END,\n",
" \"continue\": \"chat_bot\",\n",
" },\n",
")\n",
"# The input will first go to your chat bot\n",
"graph_builder.add_edge(START, \"chat_bot\")\n",
"simulation = graph_builder.compile()"
]
},
{
"cell_type": "markdown",
@@ -251,15 +392,13 @@
]
}
],
"source": ["for chunk in simulation.stream([]):\n # Print out all events aside from the final end chunk\n if END not in chunk:\n print(chunk)\n print(\"----\")"]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0",
"metadata": {},
"outputs": [],
"source": [""]
"source": [
"for chunk in simulation.stream({}):\n",
" # Print out all events aside from the final end chunk\n",
" if END not in chunk:\n",
" print(chunk)\n",
" print(\"----\")"
]
}
],
"metadata": {
@@ -359,14 +359,6 @@
" evaluation=evaluation,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26735ed2-766d-4e0a-a185-b2295a0615b8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
+266 -28
View File
@@ -32,7 +32,10 @@
"scrolled": true
},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-community langchain-openai scikit-learn"]
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-community langchain-openai scikit-learn"
]
},
{
"cell_type": "markdown",
@@ -48,7 +51,15 @@
"id": "3d1ef253-6b0c-4481-868c-e1fe84f2c8ff",
"metadata": {},
"outputs": [],
"source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\nresponse = requests.get(url)\n\nwith open(\"Chinook.db\", \"wb\") as file:\n file.write(response.content)"]
"source": [
"import requests\n",
"\n",
"url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n",
"response = requests.get(url)\n",
"\n",
"with open(\"Chinook.db\", \"wb\") as file:\n",
" file.write(response.content)"
]
},
{
"cell_type": "code",
@@ -77,7 +88,12 @@
"output_type": "execute_result"
}
],
"source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\ndb.get_usable_table_names()"]
"source": [
"from langchain_community.utilities import SQLDatabase\n",
"\n",
"db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
"db.get_usable_table_names()"
]
},
{
"cell_type": "markdown",
@@ -96,7 +112,11 @@
"id": "d9ea4e80-30e6-4d46-b480-35f0be2fb055",
"metadata": {},
"outputs": [],
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"]
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"
]
},
{
"cell_type": "markdown",
@@ -118,7 +138,9 @@
"id": "ea958e9f-ab1f-49b5-bd85-16332055297c",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import HumanMessage, SystemMessage"]
"source": [
"from langchain_core.messages import HumanMessage, SystemMessage"
]
},
{
"cell_type": "markdown",
@@ -137,7 +159,12 @@
"id": "975b039a",
"metadata": {},
"outputs": [],
"source": ["# This tool is given to the agent to look up information about a customer\ndef get_customer_info(customer_id: int):\n \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"]
"source": [
"# This tool is given to the agent to look up information about a customer\n",
"def get_customer_info(customer_id: int):\n",
" \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n",
" return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"
]
},
{
"cell_type": "code",
@@ -145,7 +172,20 @@
"id": "1d5fa446",
"metadata": {},
"outputs": [],
"source": ["customer_prompt = \"\"\"Your job is to help a user update their profile.\n\nYou only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n\nIf you are unable to help the user, you can \"\"\"\n\n\ndef get_customer_messages(messages):\n return [SystemMessage(content=customer_prompt)] + messages\n\n\ncustomer_chain = get_customer_messages | model.bind_tools([get_customer_info])"]
"source": [
"customer_prompt = \"\"\"Your job is to help a user update their profile.\n",
"\n",
"You only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n",
"\n",
"If you are unable to help the user, you can \"\"\"\n",
"\n",
"\n",
"def get_customer_messages(messages):\n",
" return [SystemMessage(content=customer_prompt)] + messages\n",
"\n",
"\n",
"customer_chain = get_customer_messages | model.bind_tools([get_customer_info])"
]
},
{
"cell_type": "markdown",
@@ -166,7 +206,19 @@
"id": "a8604a3b-b484-4b2b-a914-4236cb98c524",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_openai import OpenAIEmbeddings\n\nartists = db._execute(\"select * from Artist\")\nsongs = db._execute(\"select * from Track\")\nartist_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n).as_retriever()\nsong_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n).as_retriever()"]
"source": [
"from langchain_community.vectorstores import SKLearnVectorStore\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"artists = db._execute(\"select * from Artist\")\n",
"songs = db._execute(\"select * from Track\")\n",
"artist_retriever = SKLearnVectorStore.from_texts(\n",
" [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n",
").as_retriever()\n",
"song_retriever = SKLearnVectorStore.from_texts(\n",
" [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n",
").as_retriever()"
]
},
{
"cell_type": "markdown",
@@ -182,7 +234,16 @@
"id": "0a2a2b74",
"metadata": {},
"outputs": [],
"source": ["def get_albums_by_artist(artist):\n \"\"\"Get albums by an artist (or similar artists).\"\"\"\n docs = artist_retriever.get_relevant_documents(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
"source": [
"def get_albums_by_artist(artist):\n",
" \"\"\"Get albums by an artist (or similar artists).\"\"\"\n",
" docs = artist_retriever.get_relevant_documents(artist)\n",
" artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
" return db.run(\n",
" f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n",
" include_columns=True,\n",
" )"
]
},
{
"cell_type": "markdown",
@@ -198,7 +259,16 @@
"id": "da533f50",
"metadata": {},
"outputs": [],
"source": ["def get_tracks_by_artist(artist):\n \"\"\"Get songs by an artist (or similar artists).\"\"\"\n docs = artist_retriever.invoke(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
"source": [
"def get_tracks_by_artist(artist):\n",
" \"\"\"Get songs by an artist (or similar artists).\"\"\"\n",
" docs = artist_retriever.invoke(artist)\n",
" artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
" return db.run(\n",
" f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n",
" include_columns=True,\n",
" )"
]
},
{
"cell_type": "markdown",
@@ -214,7 +284,11 @@
"id": "b3c07010",
"metadata": {},
"outputs": [],
"source": ["def check_for_songs(song_title):\n \"\"\"Check if a song exists by its name.\"\"\"\n return song_retriever.invoke(song_title)"]
"source": [
"def check_for_songs(song_title):\n",
" \"\"\"Check if a song exists by its name.\"\"\"\n",
" return song_retriever.invoke(song_title)"
]
},
{
"cell_type": "markdown",
@@ -230,7 +304,23 @@
"id": "72a14d5c",
"metadata": {},
"outputs": [],
"source": ["song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n\nYou only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n\nWhen looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\non similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n\n\ndef get_song_messages(messages):\n return [SystemMessage(content=song_system_message)] + messages\n\n\nsong_recc_chain = get_song_messages | model.bind_tools(\n [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n)"]
"source": [
"song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n",
"\n",
"You only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n",
"\n",
"When looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\n",
"on similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n",
"\n",
"\n",
"def get_song_messages(messages):\n",
" return [SystemMessage(content=song_system_message)] + messages\n",
"\n",
"\n",
"song_recc_chain = get_song_messages | model.bind_tools(\n",
" [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n",
")"
]
},
{
"cell_type": "code",
@@ -249,7 +339,10 @@
"output_type": "execute_result"
}
],
"source": ["msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\nsong_recc_chain.invoke(msgs)"]
"source": [
"msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\n",
"song_recc_chain.invoke(msgs)"
]
},
{
"cell_type": "markdown",
@@ -267,7 +360,32 @@
"id": "73e74268",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Router(BaseModel):\n \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n\n choice: str = Field(description=\"should be one of: music, customer\")\n\n\nsystem_message = \"\"\"Your job is to help as a customer service representative for a music store.\n\nYou should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n\n- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n\nIf the user is asking or wants to ask about updating or accessing their information, send them to that route.\nIf the user is asking or wants to ask about music, send them to that route.\nOtherwise, respond.\"\"\"\n\n\ndef get_messages(messages):\n return [SystemMessage(content=system_message)] + messages"]
"source": [
"from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"\n",
"class Router(BaseModel):\n",
" \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n",
"\n",
" choice: str = Field(description=\"should be one of: music, customer\")\n",
"\n",
"\n",
"system_message = \"\"\"Your job is to help as a customer service representative for a music store.\n",
"\n",
"You should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n",
"\n",
"- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n",
"- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n",
"\n",
"If the user is asking or wants to ask about updating or accessing their information, send them to that route.\n",
"If the user is asking or wants to ask about music, send them to that route.\n",
"Otherwise, respond.\"\"\"\n",
"\n",
"\n",
"def get_messages(messages):\n",
" return [SystemMessage(content=system_message)] + messages"
]
},
{
"cell_type": "code",
@@ -275,7 +393,9 @@
"id": "ddf27314",
"metadata": {},
"outputs": [],
"source": ["chain = get_messages | model.bind_tools([Router])"]
"source": [
"chain = get_messages | model.bind_tools([Router])"
]
},
{
"cell_type": "code",
@@ -294,7 +414,10 @@
"output_type": "execute_result"
}
],
"source": ["msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\nchain.invoke(msgs)"]
"source": [
"msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\n",
"chain.invoke(msgs)"
]
},
{
"cell_type": "code",
@@ -313,7 +436,10 @@
"output_type": "execute_result"
}
],
"source": ["msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\nchain.invoke(msgs)"]
"source": [
"msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\n",
"chain.invoke(msgs)"
]
},
{
"cell_type": "code",
@@ -321,7 +447,15 @@
"id": "bd6ddd8b-7500-46a7-811d-3bcb937bda51",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import AIMessage\n\n\ndef add_name(message, name):\n _dict = message.dict()\n _dict[\"name\"] = name\n return AIMessage(**_dict)"]
"source": [
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"def add_name(message, name):\n",
" _dict = message.dict()\n",
" _dict[\"name\"] = name\n",
" return AIMessage(**_dict)"
]
},
{
"cell_type": "code",
@@ -329,7 +463,45 @@
"id": "27494de5-8345-4c23-bc0e-81e0dd5d47d8",
"metadata": {},
"outputs": [],
"source": ["import json\n\nfrom langgraph.graph import END, START\n\n\ndef _get_last_ai_message(messages):\n for m in messages[::-1]:\n if isinstance(m, AIMessage):\n return m\n return None\n\n\ndef _is_tool_call(msg):\n return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n\n\ndef _route(messages):\n last_message = messages[-1]\n if isinstance(last_message, AIMessage):\n if not last_message.tool_calls:\n return END\n else:\n if last_message.name == \"general\":\n if len(last_message.tool_calls) > 1:\n raise ValueError(\"Too many tools\")\n return last_message.tool_calls[0][\"args\"][\"choice\"]\n else:\n return \"tools\"\n last_m = _get_last_ai_message(messages)\n if last_m is None:\n return \"general\"\n if last_m.name == \"music\":\n return \"music\"\n elif last_m.name == \"customer\":\n return \"customer\"\n else:\n return \"general\""]
"source": [
"import json\n",
"\n",
"from langgraph.graph import END, START\n",
"\n",
"\n",
"def _get_last_ai_message(messages):\n",
" for m in messages[::-1]:\n",
" if isinstance(m, AIMessage):\n",
" return m\n",
" return None\n",
"\n",
"\n",
"def _is_tool_call(msg):\n",
" return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n",
"\n",
"\n",
"def _route(messages):\n",
" last_message = messages[-1]\n",
" if isinstance(last_message, AIMessage):\n",
" if not last_message.tool_calls:\n",
" return END\n",
" else:\n",
" if last_message.name == \"general\":\n",
" if len(last_message.tool_calls) > 1:\n",
" raise ValueError(\"Too many tools\")\n",
" return last_message.tool_calls[0][\"args\"][\"choice\"]\n",
" else:\n",
" return \"tools\"\n",
" last_m = _get_last_ai_message(messages)\n",
" if last_m is None:\n",
" return \"general\"\n",
" if last_m.name == \"music\":\n",
" return \"music\"\n",
" elif last_m.name == \"customer\":\n",
" return \"customer\"\n",
" else:\n",
" return \"general\""
]
},
{
"cell_type": "code",
@@ -337,7 +509,12 @@
"id": "8aec704a-46fe-4fb3-bdee-11c3bbffc370",
"metadata": {},
"outputs": [],
"source": ["from langgraph.prebuilt import ToolNode\n\ntools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\ntool_node = ToolNode(tools)"]
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\n",
"tool_node = ToolNode(tools)"
]
},
{
"cell_type": "code",
@@ -345,7 +522,16 @@
"id": "4d5b75c6-73e0-4922-a765-a15be63f869e",
"metadata": {},
"outputs": [],
"source": ["def _filter_out_routes(messages):\n ms = []\n for m in messages:\n if _is_tool_call(m):\n if m.name == \"general\":\n continue\n ms.append(m)\n return ms"]
"source": [
"def _filter_out_routes(messages):\n",
" ms = []\n",
" for m in messages:\n",
" if _is_tool_call(m):\n",
" if m.name == \"general\":\n",
" continue\n",
" ms.append(m)\n",
" return ms"
]
},
{
"cell_type": "code",
@@ -353,7 +539,13 @@
"id": "fd4dbf98-dbb3-411a-bad6-2bb334072aaf",
"metadata": {},
"outputs": [],
"source": ["from functools import partial\n\ngeneral_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\nmusic_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\ncustomer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"]
"source": [
"from functools import partial\n",
"\n",
"general_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\n",
"music_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\n",
"customer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"
]
},
{
"cell_type": "code",
@@ -361,7 +553,33 @@
"id": "dcade924",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nfrom langgraph.graph import MessageGraph\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = MessageGraph()\nnodes = {\n \"general\": \"general\",\n \"music\": \"music\",\n END: END,\n \"tools\": \"tools\",\n \"customer\": \"customer\",\n}\n# Define a new graph\nworkflow = MessageGraph()\nworkflow.add_node(\"general\", general_node)\nworkflow.add_node(\"music\", music_node)\nworkflow.add_node(\"customer\", customer_node)\nworkflow.add_node(\"tools\", tool_node)\nworkflow.add_conditional_edges(\"general\", _route, nodes)\nworkflow.add_conditional_edges(\"tools\", _route, nodes)\nworkflow.add_conditional_edges(\"music\", _route, nodes)\nworkflow.add_conditional_edges(\"customer\", _route, nodes)\nworkflow.add_conditional_edges(START, _route, nodes)\ngraph = workflow.compile()"]
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"from langgraph.graph import MessageGraph\n",
"\n",
"memory = MemorySaver()\n",
"graph = MessageGraph()\n",
"nodes = {\n",
" \"general\": \"general\",\n",
" \"music\": \"music\",\n",
" END: END,\n",
" \"tools\": \"tools\",\n",
" \"customer\": \"customer\",\n",
"}\n",
"# Define a new graph\n",
"workflow = MessageGraph()\n",
"workflow.add_node(\"general\", general_node)\n",
"workflow.add_node(\"music\", music_node)\n",
"workflow.add_node(\"customer\", customer_node)\n",
"workflow.add_node(\"tools\", tool_node)\n",
"workflow.add_conditional_edges(\"general\", _route, nodes)\n",
"workflow.add_conditional_edges(\"tools\", _route, nodes)\n",
"workflow.add_conditional_edges(\"music\", _route, nodes)\n",
"workflow.add_conditional_edges(\"customer\", _route, nodes)\n",
"workflow.add_conditional_edges(START, _route, nodes)\n",
"graph = workflow.compile()"
]
},
{
"cell_type": "code",
@@ -370,7 +588,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): what music do you have?\n"
@@ -395,7 +613,7 @@
]
},
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): how about shakira?\n"
@@ -446,7 +664,7 @@
]
},
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): hm cool\n"
@@ -483,7 +701,7 @@
]
},
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): q\n"
@@ -497,7 +715,27 @@
]
}
],
"source": ["import uuid\n\nfrom langchain_core.messages import HumanMessage\n\nfrom langgraph.graph.graph import START\n\nhistory = []\nwhile True:\n user = input(\"User (q/Q to quit): \")\n if user in {\"q\", \"Q\"}:\n print(\"AI: Byebye\")\n break\n history.append(HumanMessage(content=user))\n async for output in graph.astream(history):\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"]
"source": [
"import uuid\n",
"\n",
"from langchain_core.messages import HumanMessage\n",
"\n",
"from langgraph.graph.graph import START\n",
"\n",
"history = []\n",
"while True:\n",
" user = input(\"User (q/Q to quit): \")\n",
" if user in {\"q\", \"Q\"}:\n",
" print(\"AI: Byebye\")\n",
" break\n",
" history.append(HumanMessage(content=user))\n",
" async for output in graph.astream(history):\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")"
]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
@@ -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": {
@@ -154,7 +154,7 @@
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=memory)"]
"source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = MemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
+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": {
File diff suppressed because one or more lines are too long
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": {
+329 -24
View File
@@ -39,7 +39,10 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
},
{
"cell_type": "markdown",
@@ -55,7 +58,18 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"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\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -71,7 +85,10 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -89,7 +106,22 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# `add_messages`` essentially does this\n",
"# (with more robust handling)\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
},
{
"cell_type": "markdown",
@@ -109,7 +141,22 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]"]
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]"
]
},
{
"cell_type": "markdown",
@@ -127,7 +174,11 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
"source": [
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"tool_executor = ToolExecutor(tools)"
]
},
{
"cell_type": "markdown",
@@ -148,7 +199,11 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
},
{
"cell_type": "markdown",
@@ -166,7 +221,9 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": ["model = model.bind_tools(tools)"]
"source": [
"model = model.bind_tools(tools)"
]
},
{
"cell_type": "markdown",
@@ -201,7 +258,53 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"]
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}"
]
},
{
"cell_type": "markdown",
@@ -219,7 +322,45 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
},
{
"cell_type": "markdown",
@@ -237,7 +378,11 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()"
]
},
{
"cell_type": "markdown",
@@ -255,7 +400,12 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
},
{
"cell_type": "markdown",
@@ -282,7 +432,11 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph().draw_mermaid_png()))"
]
},
{
"cell_type": "markdown",
@@ -313,7 +467,14 @@
]
}
],
"source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"2\"}}\ninputs = [HumanMessage(content=\"hi! I'm bob\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"inputs = [HumanMessage(content=\"hi! I'm bob\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -334,7 +495,11 @@
]
}
],
"source": ["inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -358,7 +523,11 @@
]
}
],
"source": ["inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -392,7 +561,10 @@
]
}
],
"source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -427,7 +599,43 @@
"id": "5454f436-d56e-4499-9381-06192aca1b56",
"metadata": {},
"outputs": [],
"source": ["import json\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to construct message asking for verification\ndef generate_verification_message(message: AIMessage) -> None:\n \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n serialized_tool_calls = json.dumps(\n message.tool_calls,\n indent=2,\n )\n return AIMessage(\n content=(\n \"I plan to invoke the following tools, do you approve?\\n\\n\"\n \"Type 'y' if you do, anything else to stop.\\n\\n\"\n f\"{serialized_tool_calls}\"\n ),\n id=message.id,\n )\n\n\n# Helper function to stream output from the graph\ndef stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n \"\"\"Stream app, catching tool calls.\"\"\"\n tool_call_message = None\n for event in app.stream(inputs, thread, stream_mode=\"values\"):\n message = event[\"messages\"][-1]\n if isinstance(message, AIMessage) and message.tool_calls:\n tool_call_message = message\n else:\n message.pretty_print()\n\n return tool_call_message"]
"source": [
"import json\n",
"from typing import Optional\n",
"\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"# Helper function to construct message asking for verification\n",
"def generate_verification_message(message: AIMessage) -> None:\n",
" \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n",
" serialized_tool_calls = json.dumps(\n",
" message.tool_calls,\n",
" indent=2,\n",
" )\n",
" return AIMessage(\n",
" content=(\n",
" \"I plan to invoke the following tools, do you approve?\\n\\n\"\n",
" \"Type 'y' if you do, anything else to stop.\\n\\n\"\n",
" f\"{serialized_tool_calls}\"\n",
" ),\n",
" id=message.id,\n",
" )\n",
"\n",
"\n",
"# Helper function to stream output from the graph\n",
"def stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n",
" \"\"\"Stream app, catching tool calls.\"\"\"\n",
" tool_call_message = None\n",
" for event in app.stream(inputs, thread, stream_mode=\"values\"):\n",
" message = event[\"messages\"][-1]\n",
" if isinstance(message, AIMessage) and message.tool_calls:\n",
" tool_call_message = message\n",
" else:\n",
" message.pretty_print()\n",
"\n",
" return tool_call_message"
]
},
{
"cell_type": "code",
@@ -514,7 +722,43 @@
]
}
],
"source": ["import uuid\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\n\ntool_call_message = stream_app_catch_tool_calls(\n {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n thread,\n)\n\nwhile tool_call_message:\n verification_message = generate_verification_message(tool_call_message)\n verification_message.pretty_print()\n input_message = HumanMessage(input())\n if input_message.content == \"exit\":\n break\n input_message.pretty_print()\n\n # First we update the state with the verification message and the input message.\n # note that `generate_verification_message` sets the message ID to be the same\n # as the ID from the original tool call message. Updating the state with this\n # message will overwrite the previous tool call.\n snapshot = app.get_state(thread)\n snapshot.values[\"messages\"] += [verification_message, input_message]\n\n if input_message.content == \"y\":\n tool_call_message.id = str(uuid.uuid4())\n # If verified, we append the tool call message to the state\n # and resume execution.\n snapshot.values[\"messages\"] += [tool_call_message]\n app.update_state(thread, snapshot.values, as_node=\"agent\")\n else:\n # Otherwise, resume execution from the input message.\n app.update_state(thread, snapshot.values, as_node=\"__start__\")\n\n tool_call_message = stream_app_catch_tool_calls(None, thread)"]
"source": [
"import uuid\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"\n",
"tool_call_message = stream_app_catch_tool_calls(\n",
" {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n",
" thread,\n",
")\n",
"\n",
"while tool_call_message:\n",
" verification_message = generate_verification_message(tool_call_message)\n",
" verification_message.pretty_print()\n",
" input_message = HumanMessage(input())\n",
" if input_message.content == \"exit\":\n",
" break\n",
" input_message.pretty_print()\n",
"\n",
" # First we update the state with the verification message and the input message.\n",
" # note that `generate_verification_message` sets the message ID to be the same\n",
" # as the ID from the original tool call message. Updating the state with this\n",
" # message will overwrite the previous tool call.\n",
" snapshot = app.get_state(thread)\n",
" snapshot.values[\"messages\"] += [verification_message, input_message]\n",
"\n",
" if input_message.content == \"y\":\n",
" tool_call_message.id = str(uuid.uuid4())\n",
" # If verified, we append the tool call message to the state\n",
" # and resume execution.\n",
" snapshot.values[\"messages\"] += [tool_call_message]\n",
" app.update_state(thread, snapshot.values, as_node=\"agent\")\n",
" else:\n",
" # Otherwise, resume execution from the input message.\n",
" app.update_state(thread, snapshot.values, as_node=\"__start__\")\n",
"\n",
" tool_call_message = stream_app_catch_tool_calls(None, thread)"
]
},
{
"cell_type": "markdown",
@@ -535,7 +779,34 @@
"id": "03232f16-d6fe-46d0-afa0-a6f0d0bf16de",
"metadata": {},
"outputs": [],
"source": ["class State(TypedDict):\n messages: Annotated[list, add_messages]\n tool_call_message: Optional[AIMessage]\n\n\ndef call_model(state):\n messages = state[\"messages\"]\n if messages[-1].content == \"y\":\n return {\n \"messages\": [state[\"tool_call_message\"]],\n \"tool_call_message\": None,\n }\n else:\n response = model.invoke(messages)\n if response.tool_calls:\n verification_message = generate_verification_message(response)\n response.id = str(uuid.uuid4())\n return {\n \"messages\": [verification_message],\n \"tool_call_message\": response,\n }\n else:\n return {\n \"messages\": [response],\n \"tool_call_message\": None,\n }"]
"source": [
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
" tool_call_message: Optional[AIMessage]\n",
"\n",
"\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" if messages[-1].content == \"y\":\n",
" return {\n",
" \"messages\": [state[\"tool_call_message\"]],\n",
" \"tool_call_message\": None,\n",
" }\n",
" else:\n",
" response = model.invoke(messages)\n",
" if response.tool_calls:\n",
" verification_message = generate_verification_message(response)\n",
" response.id = str(uuid.uuid4())\n",
" return {\n",
" \"messages\": [verification_message],\n",
" \"tool_call_message\": response,\n",
" }\n",
" else:\n",
" return {\n",
" \"messages\": [response],\n",
" \"tool_call_message\": None,\n",
" }"
]
},
{
"cell_type": "markdown",
@@ -551,7 +822,27 @@
"id": "502dc688-c926-407e-8759-8c9e39eb4257",
"metadata": {},
"outputs": [],
"source": ["workflow = StateGraph(State)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\nworkflow.add_edge(START, \"agent\")\n\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n {\n \"continue\": \"action\",\n \"end\": END,\n },\n)\n\nworkflow.add_edge(\"action\", \"agent\")\n\napp = workflow.compile(checkpointer=memory)"]
"source": [
"workflow = StateGraph(State)\n",
"\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" {\n",
" \"continue\": \"action\",\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"app = workflow.compile(checkpointer=memory)"
]
},
{
"cell_type": "code",
@@ -584,7 +875,13 @@
]
}
],
"source": ["thread = {\"configurable\": {\"thread_id\": \"4\"}}\n\ninputs = [HumanMessage(content=\"what's the weather in sf?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"thread = {\"configurable\": {\"thread_id\": \"4\"}}\n",
"\n",
"inputs = [HumanMessage(content=\"what's the weather in sf?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -617,7 +914,11 @@
]
}
],
"source": ["inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
@@ -648,7 +949,11 @@
]
}
],
"source": ["inputs = [HumanMessage(content=\"y\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"inputs = [HumanMessage(content=\"y\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
}
],
"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": {
File diff suppressed because one or more lines are too long
@@ -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 -9
View File
@@ -33,16 +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",
"check = SqliteSaver.from_conn_string(\":memory:\")\n",
"\n",
"graph = StateGraph(input=InputState, output=OutputState)\n",
"graph.add_node(answer_node)\n",
"graph.add_edge(START, \"answer_node\")\n",
@@ -59,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": {
+48 -55
View File
@@ -174,7 +174,7 @@
"id": "b6c1dcd9-fb86-4649-81b4-ff6ce20a2e46",
"metadata": {},
"source": [
"**Notice** how the `chatbot` node function takes the current `State` as input and returns an updated `messages` list. This is the basic pattern for all LangGraph node functions.\n",
"**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key \"messages\". This is the basic pattern for all LangGraph node functions.\n",
"\n",
"The `add_messages` function in our `State` will append the llm's response messages to whatever messages are already in the state.\n",
"\n",
@@ -848,7 +848,7 @@
"\n",
"We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But before we get too ahead of ourselves, let's add checkpointing to enable multi-turn conversations.\n",
"\n",
"To get started, create a `SqliteSaver` checkpointer."
"To get started, create a `MemorySaver` checkpointer."
]
},
{
@@ -858,9 +858,9 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
"memory = MemorySaver()"
]
},
{
@@ -868,7 +868,7 @@
"id": "08d3d11a-1b42-4cbb-8e11-2a4294263d90",
"metadata": {},
"source": [
"**Notice** that we've specified `:memory` as the Sqlite DB path. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to connect to your own DB and/or use one of the other checkpointer classes.\n",
"**Notice** we're using an in-memory checkpointer. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect to your own DB.\n",
"\n",
"Next define the graph. Now that you've already built your own `BasicToolNode`, we'll replace it with LangGraph's prebuilt `ToolNode` and `tools_condition`, since these do some nice things like parallel API execution. Apart from that, the following is all copied from Part 2."
]
@@ -1199,7 +1199,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode\n",
@@ -1256,19 +1256,10 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 7,
"id": "5a81608a-373a-4339-b1c6-65b73a92b983",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/wfh/code/lc/langchain/libs/core/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The method `ChatAnthropic.bind_tools` is in beta. It is actively being worked on, so the API may change.\n",
" warn_beta(\n"
]
}
],
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
@@ -1277,12 +1268,12 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"class State(TypedDict):\n",
@@ -1320,12 +1311,12 @@
"id": "813505b2-18c1-46e9-b891-20a34232808b",
"metadata": {},
"source": [
"Now, compile the graph, specifying to `interrupt_before` the `action` node."
"Now, compile the graph, specifying to `interrupt_before` the `tools` node."
]
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 8,
"id": "b0883e32-1a39-4ce9-ae32-bbd66708fd84",
"metadata": {},
"outputs": [],
@@ -1334,14 +1325,14 @@
" checkpointer=memory,\n",
" # This is new!\n",
" interrupt_before=[\"tools\"],\n",
" # Note: can also interrupt __after__ actions, if desired.\n",
" # Note: can also interrupt __after__ tools, if desired.\n",
" # interrupt_after=[\"tools\"]\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 9,
"id": "9f318020-ab7e-415b-a5e2-eddec6d9f3a6",
"metadata": {},
"outputs": [
@@ -1354,10 +1345,10 @@
"I'm learning LangGraph. Could you do some research on it for me?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Okay, let's do some research on LangGraph:\", 'type': 'text'}, {'id': 'toolu_01Be7aRgMEv9cg6ezaFjiCry', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
"[{'text': \"Okay, let's look up some information on LangGraph:\", 'type': 'text'}, {'id': 'toolu_01XoHVKTRbipJokQorfifzvh', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" tavily_search_results_json (toolu_01Be7aRgMEv9cg6ezaFjiCry)\n",
" Call ID: toolu_01Be7aRgMEv9cg6ezaFjiCry\n",
" tavily_search_results_json (toolu_01XoHVKTRbipJokQorfifzvh)\n",
" Call ID: toolu_01XoHVKTRbipJokQorfifzvh\n",
" Args:\n",
" query: LangGraph\n"
]
@@ -1385,17 +1376,17 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 10,
"id": "9bb7af46-9b4f-4bb1-b8b9-e9ddf7dbc82c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('action',)"
"('tools',)"
]
},
"execution_count": 4,
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
@@ -1410,12 +1401,12 @@
"id": "89326046-2b11-4812-8b6d-8780306ec275",
"metadata": {},
"source": [
"**Notice** that unlike last time, the \"next\" node is set to **'action'**. We've interrupted here! Let's check the tool invocation."
"**Notice** that unlike last time, the \"next\" node is set to **'tools'**. We've interrupted here! Let's check the tool invocation."
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 11,
"id": "3facda0a-e6ad-4b28-b627-753ad8c90c15",
"metadata": {},
"outputs": [
@@ -1424,10 +1415,11 @@
"text/plain": [
"[{'name': 'tavily_search_results_json',\n",
" 'args': {'query': 'LangGraph'},\n",
" 'id': 'toolu_01Be7aRgMEv9cg6ezaFjiCry'}]"
" 'id': 'toolu_01XoHVKTRbipJokQorfifzvh',\n",
" 'type': 'tool_call'}]"
]
},
"execution_count": 5,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
@@ -1449,7 +1441,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 12,
"id": "effb95d9-b7d5-40c5-9253-253d193b23b2",
"metadata": {},
"outputs": [
@@ -1460,18 +1452,19 @@
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: tavily_search_results_json\n",
"\n",
"[{\"url\": \"https://github.com/langchain-ai/langgraph\", \"content\": \"LangGraph is a Python package that extends LangChain Expression Language with the ability to coordinate multiple chains across multiple steps of computation in a cyclic manner. It is inspired by Pregel and Apache Beam and can be used for agent-like behaviors, such as chatbots, with LLMs.\"}, {\"url\": \"https://langchain-ai.github.io/langgraph//\", \"content\": \"LangGraph is a library for building stateful, multi-actor applications with LLMs, built on top of (and intended to be used with) LangChain . It extends the LangChain Expression Language with the ability to coordinate multiple chains (or actors) across multiple steps of computation in a cyclic manner. It is inspired by Pregel and Apache Beam .\"}]\n",
"[{\"url\": \"https://langchain-ai.github.io/langgraph/tutorials/\", \"content\": \"LangGraph is a framework for building language agents as graphs. Learn how to use LangGraph to create chatbots, code assistants, planning agents, reflection agents, and more with these notebooks.\"}, {\"url\": \"https://github.com/langchain-ai/langgraph\", \"content\": \"LangGraph is a library for creating stateful, multi-actor applications with LLMs, using cycles, controllability, and persistence. Learn how to use LangGraph with examples, integration with LangChain, and streaming support.\"}]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Based on the search results, LangGraph seems to be a Python library that extends the LangChain library to enable more complex, multi-step interactions with large language models (LLMs). Some key points:\n",
"Based on the search results, LangGraph seems to be a framework for building language-based AI agents and applications using language models. It provides a modular, graph-based approach for creating chatbots, code assistants, planning agents, and other language-centric applications.\n",
"\n",
"- LangGraph allows coordinating multiple \"chains\" (or actors) over multiple steps of computation, in a cyclic manner. This enables more advanced agent-like behaviors like chatbots.\n",
"- It is inspired by distributed graph processing frameworks like Pregel and Apache Beam.\n",
"- LangGraph is built on top of the LangChain library, which provides a framework for building applications with LLMs.\n",
"Some key things I learned about LangGraph:\n",
"\n",
"So in summary, LangGraph appears to be a powerful tool for building more sophisticated applications and agents using large language models, by allowing you to coordinate multiple steps and actors in a flexible, graph-like manner. It extends the capabilities of the base LangChain library.\n",
"- It is designed to make it easier to build stateful, multi-actor applications using large language models (LLMs).\n",
"- It provides features like cycles, controllability, and persistence to help manage the complexity of these types of applications.\n",
"- LangGraph can be integrated with the LangChain library, which provides additional tools for building LLM-powered applications.\n",
"- The framework includes examples and tutorials to help get started with using LangGraph.\n",
"\n",
"Let me know if you need any clarification or have additional questions!\n"
"Overall, LangGraph seems like a promising approach for building more advanced, graph-based language applications on top of large language models. Let me know if you need any other details on LangGraph and how it works!\n"
]
}
],
@@ -1508,7 +1501,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode\n",
@@ -1543,7 +1536,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.set_entry_point(\"chatbot\")\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # This is new!\n",
@@ -1563,7 +1556,7 @@
"source": [
"## Part 5: Manually Updating the State\n",
"\n",
"In the previous section, we showed how to interrupt a graph so that a human could inspect its actions. This lets the human `read` the state, but if they want to change they agent's course, they'll need to have `write` access.\n",
"In the previous section, we showed how to interrupt a graph so that a human could inspect its actions. This lets the human `read` the state, but if they want to change their agent's course, they'll need to have `write` access.\n",
"\n",
"Thankfully, LangGraph lets you **manually update state**! Updating the state lets you control the agent's trajectory by modifying its actions (even modifying the past!). This capability is particularly useful when you want to correct the agent's mistakes, explore alternative paths, or guide the agent towards a specific goal.\n",
"\n",
@@ -1593,7 +1586,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -1627,7 +1620,7 @@
")\n",
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # This is new!\n",
@@ -2092,7 +2085,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2289,7 +2282,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # We interrupt before 'human' here instead.\n",
@@ -2539,7 +2532,7 @@
"from langchain_core.pydantic_v1 import BaseModel\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2626,7 +2619,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.set_entry_point(\"chatbot\")\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" interrupt_before=[\"human\"],\n",
@@ -2665,11 +2658,11 @@
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.messages import AIMessage, BaseMessage, ToolMessage\n",
"from langchain_core.messages import AIMessage, ToolMessage\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2756,7 +2749,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" interrupt_before=[\"human\"],\n",
@@ -3068,9 +3061,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "langgraph",
"display_name": "env",
"language": "python",
"name": "langgraph"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
File diff suppressed because one or more lines are too long
+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,
File diff suppressed because one or more lines are too long
+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,
@@ -105,10 +105,10 @@
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.messages import SystemMessage, RemoveMessage\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START, END\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"# We will add a `summary` attribute (in addition to `messages` key,\n",
@@ -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": {
+3 -11
View File
@@ -52,7 +52,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
@@ -112,11 +112,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"@tool\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": {
@@ -103,11 +103,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"@tool\n",
@@ -234,11 +234,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"@tool\n",
@@ -268,7 +268,7 @@
"\n",
"\n",
"def filter_messages(messages: list):\n",
" # This is very simple helper function which only ever uses the last two messages\n",
" # This is very simple helper function which only ever uses the last message\n",
" return messages[-1:]\n",
"\n",
"\n",
@@ -360,21 +360,13 @@
"- [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": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "langgraph-example-dev",
"language": "python",
"name": "python3"
"name": "langgraph-example-dev"
},
"language_info": {
"codemirror_mode": {
@@ -386,7 +378,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
+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",
+3 -11
View File
@@ -7,7 +7,7 @@
"source": [
"# How to pass private state\n",
"\n",
"Oftentimes, you may want nodes to be able to pass state to eachv other that should NOT be part of the main schema of the graph. This is often useful because there may be information that is not needed as input/output (and therefore doesn't really make sense to have in the main schema) but is ABSOLUTELY needed as part of the intermediate working logic.\n",
"Oftentimes, you may want nodes to be able to pass state to each other that should NOT be part of the main schema of the graph. This is often useful because there may be information that is not needed as input/output (and therefore doesn't really make sense to have in the main schema) but is ABSOLUTELY needed as part of the intermediate working logic.\n",
"\n",
"Let's take a look at an example below. In this example, we will create a RAG pipeline that:\n",
"1. Takes in a user question\n",
@@ -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": {
+13 -15
View File
@@ -11,17 +11,23 @@
"\n",
"When creating any LangGraph workflow, you can set them up to persist their state by doing using the following:\n",
"\n",
"1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver), such as the [AsyncSqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#asyncsqlitesaver)\n",
"1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver).\n",
"2. Call `compile(checkpointer=my_checkpointer)` when compiling the graph.\n",
"\n",
"Example:\n",
"There are several options for checkpointers to use.\n",
"\n",
"1. [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) is an in-memory key-value store for Graph state.\n",
"2. [SqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#sqlitesaver) allows you to save to a Sqlite db locally or in memory.\n",
"3. There are various external databases that can be used for persistence, such as [Postgres](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/), [MongoDB](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/), and [Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/).\n",
" \n",
"Here is an example using [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) in memory:\n",
"```python\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"builder = StateGraph(....)\n",
"# ... define the graph\n",
"memory = AsyncSqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"...\n",
"```\n",
@@ -361,9 +367,9 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
"memory = MemorySaver()"
]
},
{
@@ -555,14 +561,6 @@
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb20430f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -581,7 +579,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.2"
}
},
"nbformat": 4,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+306 -18
View File
@@ -20,7 +20,10 @@
"id": "969fb438",
"metadata": {},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"]
"source": [
"%%capture --no-stderr\n",
"%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"
]
},
{
"cell_type": "code",
@@ -28,7 +31,22 @@
"id": "e4958a8c",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(key: str):\n",
" if key not in os.environ:\n",
" os.environ[key] = getpass.getpass(f\"{key}:\")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"\n",
"# (Optional) For tracing\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -46,7 +64,34 @@
"id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"]
"source": [
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_openai import OpenAIEmbeddings\n",
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
"\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=100, chunk_overlap=50\n",
")\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=OpenAIEmbeddings(),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
},
{
"cell_type": "markdown",
@@ -62,7 +107,17 @@
"id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048",
"metadata": {},
"outputs": [],
"source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"]
"source": [
"from langchain.tools.retriever import create_retriever_tool\n",
"\n",
"retriever_tool = create_retriever_tool(\n",
" retriever,\n",
" \"retrieve_blog_posts\",\n",
" \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n",
")\n",
"\n",
"tools = [retriever_tool]"
]
},
{
"cell_type": "markdown",
@@ -71,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",
@@ -86,7 +141,19 @@
"id": "0e378706-47d5-425a-8ba0-57b9acffbd0c",
"metadata": {},
"outputs": [],
"source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"]
"source": [
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The add_messages function defines how an update should be processed\n",
" # Default is to replace. add_messages says \"append\"\n",
" messages: Annotated[Sequence[BaseMessage], add_messages]"
]
},
{
"attachments": {
@@ -129,7 +196,173 @@
]
}
],
"source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"]
"source": [
"from typing import Annotated, Literal, Sequence, TypedDict\n",
"\n",
"from langchain import hub\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"from langchain_core.prompts import PromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" str: A decision for whether the documents are relevant or not\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK RELEVANCE---\")\n",
"\n",
" # Data model\n",
" class grade(BaseModel):\n",
" \"\"\"Binary score for relevance check.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
"\n",
" # LLM\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
"\n",
" # LLM with tool and validation\n",
" llm_with_tool = model.with_structured_output(grade)\n",
"\n",
" # Prompt\n",
" prompt = PromptTemplate(\n",
" template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" Here is the retrieved document: \\n\\n {context} \\n\\n\n",
" Here is the user question: {question} \\n\n",
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n",
" input_variables=[\"context\", \"question\"],\n",
" )\n",
"\n",
" # Chain\n",
" chain = prompt | llm_with_tool\n",
"\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
"\n",
" question = messages[0].content\n",
" docs = last_message.content\n",
"\n",
" scored_result = chain.invoke({\"question\": question, \"context\": docs})\n",
"\n",
" score = scored_result.binary_score\n",
"\n",
" if score == \"yes\":\n",
" print(\"---DECISION: DOCS RELEVANT---\")\n",
" return \"generate\"\n",
"\n",
" else:\n",
" print(\"---DECISION: DOCS NOT RELEVANT---\")\n",
" print(score)\n",
" return \"rewrite\"\n",
"\n",
"\n",
"### Nodes\n",
"\n",
"\n",
"def agent(state):\n",
" \"\"\"\n",
" Invokes the agent model to generate a response based on the current state. Given\n",
" the question, it will decide to retrieve using the retriever tool, or simply end.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with the agent response appended to messages\n",
" \"\"\"\n",
" print(\"---CALL AGENT---\")\n",
" messages = state[\"messages\"]\n",
" model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n",
" model = model.bind_tools(tools)\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def rewrite(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" messages = state[\"messages\"]\n",
" question = messages[0].content\n",
"\n",
" msg = [\n",
" HumanMessage(\n",
" content=f\"\"\" \\n \n",
" Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
" Here is the initial question:\n",
" \\n ------- \\n\n",
" {question} \n",
" \\n ------- \\n\n",
" Formulate an improved question: \"\"\",\n",
" )\n",
" ]\n",
"\n",
" # Grader\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
" response = model.invoke(msg)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with re-phrased question\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" messages = state[\"messages\"]\n",
" question = messages[0].content\n",
" last_message = messages[-1]\n",
"\n",
" docs = last_message.content\n",
"\n",
" # Prompt\n",
" prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
" # LLM\n",
" llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n",
"\n",
" # Post-processing\n",
" def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
" # Chain\n",
" rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
" # Run\n",
" response = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n",
"prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
]
},
{
"cell_type": "markdown",
@@ -150,7 +383,48 @@
"id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"]
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the nodes we will cycle between\n",
"workflow.add_node(\"agent\", agent) # agent\n",
"retrieve = ToolNode([retriever_tool])\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieval\n",
"workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n",
"workflow.add_node(\n",
" \"generate\", generate\n",
") # Generating a response after we know the documents are relevant\n",
"# Call agent node to decide to retrieve or not\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# Decide whether to retrieve\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" # Assess agent decision\n",
" tools_condition,\n",
" {\n",
" # Translate the condition outputs to nodes in our graph\n",
" \"tools\": \"retrieve\",\n",
" END: END,\n",
" },\n",
")\n",
"\n",
"# Edges taken after the `action` node is called.\n",
"workflow.add_conditional_edges(\n",
" \"retrieve\",\n",
" # Assess agent decision\n",
" grade_documents,\n",
")\n",
"workflow.add_edge(\"generate\", END)\n",
"workflow.add_edge(\"rewrite\", \"agent\")\n",
"\n",
"# Compile\n",
"graph = workflow.compile()"
]
},
{
"cell_type": "code",
@@ -169,7 +443,15 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
},
{
"cell_type": "code",
@@ -203,15 +485,21 @@
]
}
],
"source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n 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": [""]
"source": [
"import pprint\n",
"\n",
"inputs = {\n",
" \"messages\": [\n",
" (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n",
" ]\n",
"}\n",
"for output in graph.stream(inputs):\n",
" for key, value in output.items():\n",
" pprint.pprint(f\"Output from node '{key}':\")\n",
" pprint.pprint(\"---\")\n",
" pprint.pprint(value, indent=2, width=80, depth=None)\n",
" pprint.pprint(\"\\n---\\n\")"
]
}
],
"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
+262 -13
View File
@@ -40,7 +40,10 @@
"id": "1b64a6f6-1d32-48be-92b5-66c3b04b17f7",
"metadata": {},
"outputs": [],
"source": ["%pip install -U --quiet langgraph langchain_anthropic\n%pip install -U --quiet tavily-python"]
"source": [
"%pip install -U --quiet langgraph langchain_anthropic\n",
"%pip install -U --quiet tavily-python"
]
},
{
"cell_type": "code",
@@ -48,7 +51,25 @@
"id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n\n_set_if_undefined(\"ANTHROPIC_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n",
"\n",
"_set_if_undefined(\"ANTHROPIC_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")"
]
},
{
"cell_type": "code",
@@ -56,7 +77,15 @@
"id": "567b6c4a",
"metadata": {},
"outputs": [],
"source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n# You could also use OpenAI or another provider\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"]
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n",
"# You could also use OpenAI or another provider\n",
"# from langchain_openai import ChatOpenAI\n",
"\n",
"# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
]
},
{
"cell_type": "markdown",
@@ -81,7 +110,13 @@
"id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"]
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n",
"\n",
"search = TavilySearchAPIWrapper()\n",
"tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"
]
},
{
"cell_type": "markdown",
@@ -97,7 +132,54 @@
"id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import HumanMessage, ToolMessage\nfrom langchain_core.output_parsers.openai_tools import PydanticToolsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n\n\nclass Reflection(BaseModel):\n missing: str = Field(description=\"Critique of what is missing.\")\n superfluous: str = Field(description=\"Critique of what is superfluous\")\n\n\nclass AnswerQuestion(BaseModel):\n \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n\n answer: str = Field(description=\"~250 word detailed answer to the question.\")\n reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n search_queries: list[str] = Field(\n description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n )\n\n\nclass ResponderWithRetries:\n def __init__(self, runnable, validator):\n self.runnable = runnable\n self.validator = validator\n\n def respond(self, state: list):\n response = []\n for attempt in range(3):\n response = self.runnable.invoke(\n {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n )\n try:\n self.validator.invoke(response)\n return response\n except ValidationError as e:\n state = state + [\n response,\n ToolMessage(\n content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n + self.validator.schema_json()\n + \" Respond by fixing all validation errors.\",\n tool_call_id=response.tool_calls[0][\"id\"],\n ),\n ]\n return response"]
"source": [
"from langchain_core.messages import HumanMessage, ToolMessage\n",
"from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n",
"\n",
"\n",
"class Reflection(BaseModel):\n",
" missing: str = Field(description=\"Critique of what is missing.\")\n",
" superfluous: str = Field(description=\"Critique of what is superfluous\")\n",
"\n",
"\n",
"class AnswerQuestion(BaseModel):\n",
" \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n",
"\n",
" answer: str = Field(description=\"~250 word detailed answer to the question.\")\n",
" reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n",
" search_queries: list[str] = Field(\n",
" description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n",
" )\n",
"\n",
"\n",
"class ResponderWithRetries:\n",
" def __init__(self, runnable, validator):\n",
" self.runnable = runnable\n",
" self.validator = validator\n",
"\n",
" def respond(self, state: list):\n",
" response = []\n",
" for attempt in range(3):\n",
" response = self.runnable.invoke(\n",
" {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
" )\n",
" try:\n",
" self.validator.invoke(response)\n",
" return response\n",
" except ValidationError as e:\n",
" state = state + [\n",
" response,\n",
" ToolMessage(\n",
" content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n",
" + self.validator.schema_json()\n",
" + \" Respond by fixing all validation errors.\",\n",
" tool_call_id=response.tool_calls[0][\"id\"],\n",
" ),\n",
" ]\n",
" return response"
]
},
{
"cell_type": "code",
@@ -114,7 +196,40 @@
]
}
],
"source": ["import datetime\n\nactor_prompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are expert researcher.\nCurrent time: {time}\n\n1. {first_instruction}\n2. Reflect and critique your answer. Be severe to maximize improvement.\n3. Recommend search queries to research information and improve your answer.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"user\",\n \"\\n\\n<system>Reflect on the user's original question and the\"\n \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n ),\n ]\n).partial(\n time=lambda: datetime.datetime.now().isoformat(),\n)\ninitial_answer_chain = actor_prompt_template.partial(\n first_instruction=\"Provide a detailed ~250 word answer.\",\n function_name=AnswerQuestion.__name__,\n) | llm.bind_tools(tools=[AnswerQuestion])\nvalidator = PydanticToolsParser(tools=[AnswerQuestion])\n\nfirst_responder = ResponderWithRetries(\n runnable=initial_answer_chain, validator=validator\n)"]
"source": [
"import datetime\n",
"\n",
"actor_prompt_template = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are expert researcher.\n",
"Current time: {time}\n",
"\n",
"1. {first_instruction}\n",
"2. Reflect and critique your answer. Be severe to maximize improvement.\n",
"3. Recommend search queries to research information and improve your answer.\"\"\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"user\",\n",
" \"\\n\\n<system>Reflect on the user's original question and the\"\n",
" \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n",
" ),\n",
" ]\n",
").partial(\n",
" time=lambda: datetime.datetime.now().isoformat(),\n",
")\n",
"initial_answer_chain = actor_prompt_template.partial(\n",
" first_instruction=\"Provide a detailed ~250 word answer.\",\n",
" function_name=AnswerQuestion.__name__,\n",
") | llm.bind_tools(tools=[AnswerQuestion])\n",
"validator = PydanticToolsParser(tools=[AnswerQuestion])\n",
"\n",
"first_responder = ResponderWithRetries(\n",
" runnable=initial_answer_chain, validator=validator\n",
")"
]
},
{
"cell_type": "code",
@@ -122,7 +237,10 @@
"id": "5922e1fe-7533-4f41-8b1d-d812707c1968",
"metadata": {},
"outputs": [],
"source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"]
"source": [
"example_question = \"Why is reflection useful in AI?\"\n",
"initial = first_responder.respond([HumanMessage(content=example_question)])"
]
},
{
"cell_type": "markdown",
@@ -140,7 +258,38 @@
"id": "2605fd8d-c663-446f-ba25-751190195749",
"metadata": {},
"outputs": [],
"source": ["revise_instructions = \"\"\"Revise your previous answer using the new information.\n - You should use the previous critique to add important information to your answer.\n - You MUST include numerical citations in your revised answer to ensure it can be verified.\n - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n - [1] https://example.com\n - [2] https://example.com\n - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n\"\"\"\n\n\n# Extend the initial answer schema to include references.\n# Forcing citation in the model encourages grounded responses\nclass ReviseAnswer(AnswerQuestion):\n \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n\n cite your reflection with references, and finally\n add search queries to improve the answer.\"\"\"\n\n references: list[str] = Field(\n description=\"Citations motivating your updated answer.\"\n )\n\n\nrevision_chain = actor_prompt_template.partial(\n first_instruction=revise_instructions,\n function_name=ReviseAnswer.__name__,\n) | llm.bind_tools(tools=[ReviseAnswer])\nrevision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n\nrevisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"]
"source": [
"revise_instructions = \"\"\"Revise your previous answer using the new information.\n",
" - You should use the previous critique to add important information to your answer.\n",
" - You MUST include numerical citations in your revised answer to ensure it can be verified.\n",
" - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n",
" - [1] https://example.com\n",
" - [2] https://example.com\n",
" - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n",
"\"\"\"\n",
"\n",
"\n",
"# Extend the initial answer schema to include references.\n",
"# Forcing citation in the model encourages grounded responses\n",
"class ReviseAnswer(AnswerQuestion):\n",
" \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n",
"\n",
" cite your reflection with references, and finally\n",
" add search queries to improve the answer.\"\"\"\n",
"\n",
" references: list[str] = Field(\n",
" description=\"Citations motivating your updated answer.\"\n",
" )\n",
"\n",
"\n",
"revision_chain = actor_prompt_template.partial(\n",
" first_instruction=revise_instructions,\n",
" function_name=ReviseAnswer.__name__,\n",
") | llm.bind_tools(tools=[ReviseAnswer])\n",
"revision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n",
"\n",
"revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"
]
},
{
"cell_type": "code",
@@ -159,7 +308,25 @@
"output_type": "execute_result"
}
],
"source": ["import json\n\nrevised = revisor.respond(\n [\n HumanMessage(content=example_question),\n initial,\n ToolMessage(\n tool_call_id=initial.tool_calls[0][\"id\"],\n content=json.dumps(\n tavily_tool.invoke(\n {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n )\n ),\n ),\n ]\n)\nrevised"]
"source": [
"import json\n",
"\n",
"revised = revisor.respond(\n",
" [\n",
" HumanMessage(content=example_question),\n",
" initial,\n",
" ToolMessage(\n",
" tool_call_id=initial.tool_calls[0][\"id\"],\n",
" content=json.dumps(\n",
" tavily_tool.invoke(\n",
" {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n",
" )\n",
" ),\n",
" ),\n",
" ]\n",
")\n",
"revised"
]
},
{
"cell_type": "markdown",
@@ -177,7 +344,24 @@
"id": "fccd6a17",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.tools import StructuredTool\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef run_queries(search_queries: list[str], **kwargs):\n \"\"\"Run the generated queries.\"\"\"\n return tavily_tool.batch([{\"query\": query} for query in search_queries])\n\n\ntool_node = ToolNode(\n [\n StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n ]\n)"]
"source": [
"from langchain_core.tools import StructuredTool\n",
"\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"\n",
"def run_queries(search_queries: list[str], **kwargs):\n",
" \"\"\"Run the generated queries.\"\"\"\n",
" return tavily_tool.batch([{\"query\": query} for query in search_queries])\n",
"\n",
"\n",
"tool_node = ToolNode(\n",
" [\n",
" StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n",
" StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
@@ -196,7 +380,56 @@
"id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1",
"metadata": {},
"outputs": [],
"source": ["from typing import Literal\n\nfrom langgraph.graph import END, MessageGraph, START\n\nMAX_ITERATIONS = 5\nbuilder = MessageGraph()\nbuilder.add_node(\"draft\", first_responder.respond)\n\n\nbuilder.add_node(\"execute_tools\", tool_node)\nbuilder.add_node(\"revise\", revisor.respond)\n# draft -> execute_tools\nbuilder.add_edge(\"draft\", \"execute_tools\")\n# execute_tools -> revise\nbuilder.add_edge(\"execute_tools\", \"revise\")\n\n# Define looping logic:\n\n\ndef _get_num_iterations(state: list):\n i = 0\n for m in state[::-1]:\n if m.type not in {\"tool\", \"ai\"}:\n break\n i += 1\n return i\n\n\ndef event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n # in our case, we'll just stop after N plans\n num_iterations = _get_num_iterations(state)\n if num_iterations > MAX_ITERATIONS:\n return END\n return \"execute_tools\"\n\n\n# revise -> execute_tools OR end\nbuilder.add_conditional_edges(\"revise\", event_loop)\nbuilder.add_edge(START, \"draft\")\ngraph = builder.compile()"]
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"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",
"MAX_ITERATIONS = 5\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"draft\", first_responder.respond)\n",
"\n",
"\n",
"builder.add_node(\"execute_tools\", tool_node)\n",
"builder.add_node(\"revise\", revisor.respond)\n",
"# draft -> execute_tools\n",
"builder.add_edge(\"draft\", \"execute_tools\")\n",
"# execute_tools -> revise\n",
"builder.add_edge(\"execute_tools\", \"revise\")\n",
"\n",
"# Define looping logic:\n",
"\n",
"\n",
"def _get_num_iterations(state: list):\n",
" i = 0\n",
" for m in state[::-1]:\n",
" if m.type not in {\"tool\", \"ai\"}:\n",
" break\n",
" i += 1\n",
" return i\n",
"\n",
"\n",
"def event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n",
" # in our case, we'll just stop after N plans\n",
" num_iterations = _get_num_iterations(state)\n",
" if num_iterations > MAX_ITERATIONS:\n",
" return END\n",
" return \"execute_tools\"\n",
"\n",
"\n",
"# revise -> execute_tools OR end\n",
"builder.add_conditional_edges(\"revise\", event_loop)\n",
"builder.add_edge(START, \"draft\")\n",
"graph = builder.compile()"
]
},
{
"cell_type": "code",
@@ -215,7 +448,15 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
},
{
"cell_type": "code",
@@ -330,7 +571,15 @@
]
}
],
"source": ["events = graph.stream(\n [HumanMessage(content=\"How should we handle the climate crisis?\")],\n stream_mode=\"values\",\n)\nfor i, step in enumerate(events):\n print(f\"Step {i}\")\n step[-1].pretty_print()"]
"source": [
"events = graph.stream(\n",
" [HumanMessage(content=\"How should we handle the climate crisis?\")],\n",
" stream_mode=\"values\",\n",
")\n",
"for i, step in enumerate(events):\n",
" print(f\"Step {i}\")\n",
" step[-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -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": {
+28 -16
View File
@@ -14,9 +14,24 @@
"Below is a simple toy example."
]
},
{
"cell_type": "markdown",
"id": "95301021-1db9-426f-807c-ec5b37bd5a9d",
"metadata": {},
"source": [
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">ASYNC IN PYTHON<=3.10</p>\n",
" <p>\n",
"Any Langchain RunnableLambda, a RunnableGenerator, or Tool that invokes other runnables and is running async in python<=3.10, will have to propagate callbacks to child objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.\n",
" \n",
"This is a common reason why you may fail to see events being emitted from custom runnables or tools.\n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 1,
"id": "486a01a0",
"metadata": {},
"outputs": [],
@@ -51,7 +66,12 @@
" messages = []\n",
" # Tagging a node makes it easy to filter out which events to include in your stream\n",
" # It's completely optional, but useful if you have many functions with similar names\n",
" gen = RunnableGenerator(my_generator).with_config(tags=[\"should_stream\"])\n",
" gen = RunnableGenerator(my_generator).with_config(\n",
" tags=[\"should_stream\"],\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",
" return {\"messages\": [AIMessage(content=\" \".join(messages))]}\n",
@@ -65,7 +85,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 2,
"id": "ce773a40",
"metadata": {},
"outputs": [
@@ -80,7 +100,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
"/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
" warn_beta(\n"
]
}
@@ -89,7 +109,7 @@
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
"async for event in app.astream_events({\"messages\": inputs}, version=\"v1\"):\n",
"async for event in app.astream_events({\"messages\": inputs}, version=\"v2\"):\n",
" kind = event[\"event\"]\n",
" tags = event.get(\"tags\", [])\n",
" if kind == \"on_chain_stream\" and \"should_stream\" in tags:\n",
@@ -100,21 +120,13 @@
" # So we only print non-empty content\n",
" print(data, end=\"|\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2c7b7902-2d80-4bf9-91c1-737b749e58a3",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "langgraph",
"language": "python",
"name": "python3"
"name": "langgraph"
},
"language_info": {
"codemirror_mode": {
@@ -126,7 +138,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
@@ -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
+1 -1
View File
@@ -246,7 +246,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
"source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"]
},
{
"cell_type": "code",
@@ -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",
+2 -2
View File
@@ -574,7 +574,7 @@
}
],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import START, END, StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"from IPython.display import Image, display\n",
@@ -597,7 +597,7 @@
"builder.add_edge(\"tools\", \"assistant\")\n",
"\n",
"# The checkpointer lets the graph persist its state\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"react_graph = builder.compile(checkpointer=memory)\n",
"\n",
"# Show\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",
@@ -253,7 +256,7 @@
"\n",
"\n",
"from IPython.display import Image, display\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import END, START, StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -275,7 +278,7 @@
"builder.add_edge(\"tools\", \"assistant\")\n",
"\n",
"# The checkpointer lets the graph persist its state\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"memory = MemorySaver()\n",
"react_graph = builder.compile(checkpointer=memory)\n",
"\n",
"# Show\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",

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