Compare commits

..
Author SHA1 Message Date
William FHandGitHub 277c341817 Configure loopback transports (#3522) 2025-02-19 15:58:57 -08:00
mathislindnerandGitHub 88d0f41c55 docs: Update map-reduce-fixed-typo.ipynb (#3509)
fixed a typo:
We will use this an edge in the graph
to 
We will use this as an edge in the graph
2025-02-19 17:28:21 -05:00
Eugene YurtsevandGitHub 0a4953c4bc Update index.md (#3519) 2025-02-19 21:52:17 +00:00
Eugene YurtsevandGitHub 059e16789c docs: fix anchor links (#3518) 2025-02-19 16:48:41 -05:00
David DuongandGitHub 41b36dcf3f docs: playground studio integration (#3514) 2025-02-19 19:49:37 +01:00
Arjun Natarajan b39dcd7fad pr feedback 2025-02-19 10:43:02 -08:00
Arjun Natarajan c0ad92b6db update title 2025-02-19 10:20:13 -08:00
Eugene YurtsevandGitHub b8d25bc0ed docs build: test api link generation (#3513) 2025-02-19 13:15:53 -05:00
Arjun Natarajan 7884401ec8 docs for playground studio integration 2025-02-19 09:43:21 -08:00
David DuongandGitHub a3bc029344 feat(sdk-js/react): make configurable typed via generics (#3511) 2025-02-19 18:19:51 +01:00
William FHandGitHub 4875973ac5 [CLI] Support http config (#3505)
Right now requires that the file be in one of the local
packages/dependencies.
2025-02-19 08:52:00 -08:00
Tat Dat Duong 15e67fdd57 feat(sdk-js/react): make configurable typed 2025-02-19 17:27:26 +01:00
Eugene YurtsevandGitHub 1393270664 concepts: add durable execution to nav (#3508) 2025-02-19 16:04:55 +00:00
Nuno CamposandGitHub 6e0295b4de feat(ci): only run lint/test if files have changed (#3507) 2025-02-19 08:04:42 -08:00
Tat Dat Duong 4aadfccf95 feat(ci): only run lint/test if files have changed 2025-02-19 16:38:35 +01:00
David DuongandGitHub 70b2da1301 fix(docs): add install command for sdk-js/react (#3506) 2025-02-19 16:33:47 +01:00
YkohandGitHub 209864da45 docs: Remove unused imports (#3500)
This PR removes the unused imports Literal and TypedDict from the typing
module.

These imports were not referenced in the code.

```python
from typing import Literal, TypedDict
```
2025-02-19 15:33:13 +00:00
Tat Dat Duong 8b6ef35f0c fix(docs): add install command for sdk-js/react 2025-02-19 16:24:34 +01:00
Eugene YurtsevandGitHub a01537d1a5 docs: concepts durable execution (#3355)
Conceptual page for durable execution
2025-02-19 10:15:26 -05:00
Vadym BardaandGitHub 05b4a30c04 langgraph: optionally add structured_response key to agent state in create_react_agent (#3493) 2025-02-19 15:06:46 +00:00
David DuongandGitHub 93e10fbe15 fix(sdk-js): mark ui-related peer deps as optional (#3503) 2025-02-19 15:53:45 +01:00
Tat Dat Duong f6989f2c7d Bump to 0.0.43 2025-02-19 15:44:03 +01:00
Tat Dat Duong c4f8346479 fix(sdk-js): mark ui-related peer deps as optional 2025-02-19 15:43:36 +01:00
William FHandGitHub 82c9d4b368 Update docstrings for command & send (#3492) 2025-02-19 05:28:45 -08:00
David DuongandGitHub 647f22fdd9 fix(sdk-js): add docs to gitignore path (#3498) 2025-02-19 09:11:19 +01:00
Tat Dat Duong 0aba1b4887 fix(sdk-js): add docs to gitignore path 2025-02-19 09:01:43 +01:00
Eugene YurtsevandGitHub 4c0c52d996 docs: revert changes to api reference generation (#3494)
* Temporarily revert. Need to add unit tests tomorrow and can then
restore
2025-02-19 02:01:03 +00:00
Andrew NguonlyandGitHub 580fe68c8e docs: Update note about LangGraph Deployments view in self-hosted deployment options (#3491) 2025-02-18 17:55:57 -08:00
YkohandGitHub 2f26268ff4 Fix issues in chatbot simulation evaluation tutorial (#3462)
## Description
While following the LangChain tutorial on [chatbot simulation
evaluation](https://langchain-ai.github.io/langgraph/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation/),
I encountered some issues and made the following fixes to ensure proper
functionality:

1. Updated deprecated `run_on_dataset` to `evaluate` to resolve
PydanticUserError
- Following the migration guide:
https://docs.smith.langchain.com/old/evaluation/migration

2. Added missing `langchain_community` import

3. Added required `simulation_utils.py` file to docs directory
- Source:
https://github.com/langchain-ai/langgraph/blob/main/examples/chatbot-simulation-evaluation/simulation_utils.py

## Testing
Successfully ran the `langsmith-agent-simulation-evaluation.ipynb`
notebook without any errors.
2025-02-19 00:26:50 +00:00
Eugene YurtsevandGitHub 84f8a43f6e docs: update measurement id (#3487) 2025-02-19 00:11:34 +00:00
Vadym BardaandGitHub 7ce0e3e15e langgraph: release 0.2.74 (#3489) 2025-02-18 18:56:04 -05:00
Vadym BardaandGitHub 1e6d958434 langgraph: fallback on no-op writer in get_stream_writer (#3488) 2025-02-18 23:53:11 +00:00
Vadym BardaandGitHub 68ba8aa393 docs: add mcp adapters (#3485) 2025-02-18 17:47:55 -05:00
Eugene YurtsevandGitHub e6a0f08561 docs: update prebuilt stub (#3486) 2025-02-18 17:09:33 -05:00
Eugene YurtsevandGitHub f38784a291 ci: remove markddown-exec from docs pipeline (#3482)
This PR removes the following changes:
* notebooks that were converted to markdown
* mkdocs.yml file to reference the ipython notebooks rather than the
markdown files
* Makefile install vercel reverted
* hooks for markdown-exec
* notebook conversion jinja2 templates (for converting notebooks to
markdown exec format)
2025-02-18 16:16:40 -05:00
Nuno CamposandGitHub fc834127fd checkpoint: Fix memory leak in checkpoint serializer (#3481)
- Packer instances can't be kept in memory as they retain memory after
used
2025-02-18 11:56:15 -08:00
Nuno Campos f3a0cbf294 Format 2025-02-18 11:46:48 -08:00
Nuno Campos f4fec76257 checkpoint: Fix memory leak in checkpoint serializer
- Packer instances can't be kept in memory as they retain memory after used
2025-02-18 11:44:26 -08:00
Eugene YurtsevandGitHub 457641f15e docs: remove beta from functional api (#3475) 2025-02-18 13:33:47 -05:00
Eugene YurtsevandGitHub 9f15e15e26 reference: remove incorrect example from pregel class (#3476)
Remove incorrect example from Pregel class. Will follow later this week
with better docs
2025-02-18 13:33:15 -05:00
Yan ZhaoandGitHub cb9989030e Very minor typo in docstring of State -> add_node (#3461)
Hi, I am a student and was going through the tutorial. While trying to
understand the different components by reading the docstring found this
super minor typo 😄 . I hope to contribute more meaningful changes in
future 😸
2025-02-18 13:05:26 -05:00
a008725c06 Ensure remote respects recursion_limit param if it's passed (#3470)
Currently when using RemoteGraph the recursion_limit cannot be set, due
to the sanitize_config.

---------

Co-authored-by: Simon Moxon <simon@together.ly>
Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
2025-02-18 17:49:31 +00:00
a93f17e624 langgraph(prebuilt): allow a PromptTemplate in react agent (#3463)
Currently a ChatPromptTemplate cannot be used as a `prompt` for
`create_react_agent` without complaints from type checkers, although it
is supported by `model` as input.

Add the missing types to remove the warning.

---------

Co-authored-by: vbarda <vadym@langchain.dev>
2025-02-18 17:39:46 +00:00
William FHandGitHub c7eddcc6e3 Add langmem link (#3473) 2025-02-18 08:15:09 -08:00
David DuongandGitHub 4cdad6c206 fix(cli): use '{{json .}}' format instead of 'json' (#3479)
Related to https://github.com/langchain-ai/langgraph/issues/1319
2025-02-18 16:01:38 +01:00
Tat Dat Duong b9fb155d59 Bump to 0.1.52 2025-02-18 15:51:57 +01:00
Tat Dat Duong 8b817a5b16 Fix lint 2025-02-18 15:51:05 +01:00
Tat Dat Duong 3a67f3a3eb fix(cli): use '{{json .}}' format instead of 'json'
Related to https://github.com/langchain-ai/langgraph/issues/1319
2025-02-18 14:58:24 +01:00
Nuno CamposandGitHub 1283539500 Add CONFIG_KEY_RUNNER_SUBMIT (#3474) 2025-02-17 17:42:35 -08:00
Nuno Campos 69ad42cac5 Add CONFIG_KEY_RUNNER_SUBMIT 2025-02-17 17:33:38 -08:00
Nuno CamposandGitHub 264b02e3ad cli: Add support for dependencies in parent directories (#3472)
- Now supporting local dependencies in directories that are not
contained in the docker context (ie. outside the folder containing
langgraph.json)
- This is achieved by passing each parent directorty as an additional
context to docker build
- This makes it a lot easier to build projects contained in monorepos
where you need to include some sibling/parent folder as a dependency
- Also include additional comments in the generated dockerfile to
delimit each section
2025-02-17 17:21:41 -08:00
Nuno Campos b5c659bc9f Resolve 2025-02-17 17:12:06 -08:00
Nuno Campos 4623f7b5da Fix 2025-02-17 17:09:31 -08:00
William Fu-Hinthorn 1641402341 Add sibling dep test 2025-02-17 14:59:13 -08:00
Nuno Campos d03ead2f43 Fix path in assertion 2025-02-17 11:47:38 -08:00
Nuno Campos 5a8624fdfd cli: Add support for dependencies in parent directories
- Now supporting local dependencies in directories that are not contained in the docker context (ie. outside the folder containing langgraph.json)
- This is achieved by passing each parent directorty as an additional context to docker build
- This makes it a lot easier to build projects contained in monorepos where you need to include some sibling/parent folder as a dependency
- Also include additional comments in the generated dockerfile to delimit each section
2025-02-17 11:43:55 -08:00
David DuongandGitHub c44ec55095 fix(docs): broken assistant-ui link (#3467) 2025-02-17 04:50:11 +01:00
Tat Dat Duong 25d682cc9e fix(docs): broken assistant-ui link 2025-02-17 04:38:22 +01:00
Vadym BardaandGitHub 6f37330141 langgraph: release 0.2.73 (#3456) 2025-02-15 16:13:30 -05:00
Vadym BardaandGitHub 1356a0ba42 langgraph: better typing for node functions (#3455) 2025-02-15 21:11:33 +00:00
Nuno Campos 9786be1ff7 Update requirement 2025-02-14 19:01:50 -08:00
Nuno CamposandGitHub c2a129c882 Exclude complex values from checkpoint metadata (#3448) 2025-02-14 18:51:48 -08:00
Nuno Campos 62f004fd28 Lint 2025-02-14 18:42:40 -08:00
Nuno Campos d4b22ac1d4 Fix postgres tests 2025-02-14 18:40:30 -08:00
Nuno Campos 0415c02b40 Fix sqlite tests 2025-02-14 18:34:06 -08:00
Nuno Campos e8665f84e7 Update 2025-02-14 18:31:03 -08:00
Nuno Campos 8ff5e79e70 Lint 2025-02-14 18:27:49 -08:00
Nuno Campos 7f4822931e Update tests 2025-02-14 18:24:48 -08:00
Nuno Campos 9706211aca Exclude complex values from checkpoint metadata 2025-02-14 17:35:14 -08:00
Ben BurnsandGitHub 405da6d507 chore(docs): enable analytics, add consent banner & copyright notice (#3447) 2025-02-15 00:54:31 +00:00
Nuno Campos bf7252cadc checkpoint 2.0.14 2025-02-14 12:28:04 -08:00
Nuno CamposandGitHub e33bac6737 Fix busy loop in AsyncBatchedBaseStore (#3445)
- while loop w asyncio.sleep(0) takes up cpu
2025-02-14 12:12:28 -08:00
Nuno Campos da97d2e1ba Fix 2025-02-14 12:03:03 -08:00
Nuno Campos 6baf320d8e Fix 2025-02-14 10:43:03 -08:00
Nuno Campos a064ccdca1 Lint 2025-02-14 10:40:09 -08:00
Nuno Campos b5479b48bf Lint 2025-02-14 10:39:34 -08:00
Nuno Campos 9dbcb03185 Fix busy loop in AsyncBatchedBaseStore
- while loop w asyncio.sleep(0) takes up cpu
2025-02-14 10:36:02 -08:00
Eugene YurtsevandGitHub f2faa39ca9 add api reference for Pregel (#3437) 2025-02-13 21:07:43 -05:00
Eugene YurtsevandGitHub b4f6cdf01f docs: remove astream events from streaming conceptual guide (#3438) 2025-02-13 21:07:30 -05:00
Vadym BardaandGitHub cd976e779d docs: update stateless runs how-to guide (#3439) 2025-02-13 20:27:02 -05:00
Eugene YurtsevandGitHub c1f337f50b docs: add check code output for result="ansi" (#3435)
* Add ast parsing to determine whether we should include result="ansi".
It's not meant to be perfect, but will hopefully catch the most common
cases. Still requires manual review.
* Ideally we could suppress output in markdown-exec in the future.
2025-02-14 01:07:40 +00:00
David DuongandGitHub 31d3ceaf6d feat(sdk-js): bump to 0.0.42 (#3436) 2025-02-13 16:27:04 -08:00
Tat Dat Duong a48844632d feat(sdk-js): bump to 0.0.42 2025-02-13 16:17:02 -08:00
Eugene YurtsevandGitHub 437891aa4f docs: handle more links (#3434) 2025-02-13 23:43:29 +00:00
Eugene YurtsevandGitHub 3e1bbd3123 docs: cell magic to shell block conversion (#3433)
* add handling for blocks that use magic commands like %pip to convert
them into bash
* Apply new logic to another notebook
2025-02-13 18:23:49 -05:00
Eugene YurtsevandGitHub 9111449ffd docs: handle input() and cell magic for notebook conversion (#3432) 2025-02-13 22:29:32 +00:00
Eugene YurtsevandGitHub 77d7c00ce8 docs: fix up edge cases with links in notebooks (#3414)
* Adds another notebook conversion
* Fix up some edge cases for handling links in notebooks. Notebooks
links were using a different convention than markdown links.

We'll need to push additional logic to use an appropriate suffix (.md or
.ipynb) for cross-references between how-to guides (though these should
be rare).
2025-02-13 15:33:55 -05:00
Eugene YurtsevandGitHub 91725d742d docs: add unit tests to build pipeline (#3427)
* Add testing step to to docs build pipeline
* Requires updating import structure in some place
* Add simple unit test to cover some logic with highlights
2025-02-13 15:32:42 -05:00
David DuongandGitHub d73a4539ec feat(sdk-js): add docs for new useStream hook (#3420)
- **Add basic docs**
- **Add docs**
- **feat(sdk-js): add docs, how-to guide**
2025-02-13 11:32:03 -08:00
Nuno CamposandGitHub 15e2df6da5 Add LGP Arch page (#3428) 2025-02-13 11:22:29 -08:00
Tat Dat Duong ce6b396186 Update index page as well 2025-02-13 11:16:32 -08:00
Tat Dat Duong d2ab02edf1 Add shoutout to CopilotKit and assistant-ui 2025-02-13 11:15:45 -08:00
Nuno Campos 0aef9424a8 Add link 2025-02-13 11:12:49 -08:00
Nuno Campos ed23288e5b Add LGP Arch page 2025-02-13 11:09:31 -08:00
Tat Dat Duong 7ec8a4cb4d Update type definitions 2025-02-13 10:57:00 -08:00
David DuongandGitHub 2d9ca3045e feat(sdk-js): expose branches (#3426) 2025-02-13 10:45:01 -08:00
David DuongandGitHub b065c54871 feat(sdk-js): make "messages" the default key (#3425) 2025-02-13 10:39:17 -08:00
Tat Dat Duong 3c0a677c90 feat(sdk-js): make "messages" the default key 2025-02-13 10:36:22 -08:00
Tat Dat Duong 7e4852373d Fix broken links 2025-02-13 10:23:11 -08:00
Tat Dat Duong 422b2ba7f0 Update docs 2025-02-13 10:18:09 -08:00
Tat Dat Duong 2c66ac869d feat(sdk-js): expose branches 2025-02-13 10:17:42 -08:00
Matt SteadmanandGitHub 1ef7121100 docs: Fix grammar in LangGraph Glossery (#3407) 2025-02-13 13:05:54 -05:00
William FHandGitHub a53287f3d8 Clarify custom auth <-> deployment options (#3423) 2025-02-13 17:38:38 +00:00
Vadym BardaandGitHub da96925ecb ci: install mkdocs-insiders only for internal PRs (#3422) 2025-02-13 12:24:30 -05:00
Tat Dat Duong f3403eab48 Add link ref 2025-02-13 07:55:46 -08:00
Tat Dat Duong 208d9d165d feat(sdk-js): add docs, how-to guide 2025-02-13 07:44:01 -08:00
Tat Dat Duong 49c74dd569 Add docs 2025-02-13 07:12:15 -08:00
Tat Dat Duong 2d97af57f8 Add basic docs 2025-02-13 07:08:35 -08:00
b310ce07bc docs: add scripts for notebook conversion (#3406)
* Update notebook conversion code
* Convert one more file

---------

Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-02-13 06:48:27 +00:00
Vadym BardaandGitHub 65976f311f langgraph: release 0.2.72 (#3413) 2025-02-13 00:14:54 -05:00
Nuno CamposandGitHub 80c9d61fbd langgraph: fix None handling for pydantic state updates (#3411) 2025-02-12 21:05:02 -08:00
ZapironandGitHub a578c7b137 docs: fix grammatical error for navigating node to parent section (#3356)
Add within to better explain moving from one subgraph to another
2025-02-12 23:59:59 -05:00
vbarda 04e8342d97 pydantic v1 2025-02-12 20:46:12 -08:00
vbarda b0e11ae524 update 2025-02-12 20:02:26 -08:00
Vadym BardaandGitHub d45253cee8 checkpoint: release libraries (#3412) 2025-02-12 22:44:44 -05:00
Vadym BardaandGitHub 1377e3b6ba checkpoint: combine metadata when writing checkpoints (#3404) 2025-02-13 03:24:41 +00:00
vbarda c36323cba8 3.9 2025-02-12 19:17:05 -08:00
148cf52981 chore(docs): notebook convert script + 1 conversion (#3390)
* Adds a conversion script from ipython notebook to markdown.
* Replaces one ipython notebook (create react agent) with a markdown file for testing.

---------

Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-02-13 02:38:15 +00:00
Eugene YurtsevandGitHub 9010303245 docs: add update prebuilt on docs deploy (#3410)
logic executed on every PR which involves a network request per package
to download stats from pypi.
2025-02-12 20:59:15 -05:00
vbarda 661476e88d langgraph: fix None handling for pydantic state updates 2025-02-12 17:53:12 -08:00
3397d8908f docs: adding Breeze Agent for third party agent page (#3352)
Adding open source web researcher agent to the third party page

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: Eugene Yurtsev <eugene@langchain.dev>
2025-02-12 20:15:22 -05:00
Nuno Campos e005f0472b sdk-js 0.0.40 2025-02-12 16:53:02 -08:00
David DuongandGitHub 18a1d60e45 feat(sdk-js): further 2x improvement when SSE (#3408) 2025-02-12 16:09:18 -08:00
Tat Dat Duong 5091d5e9fe Remove ts-node 2025-02-12 15:56:01 -08:00
Tat Dat Duong da1fae0c72 Cleanup 2025-02-12 15:55:10 -08:00
Tat Dat Duong 7db81d72dc Cleanup 2025-02-12 15:55:10 -08:00
Tat Dat Duong 4ce387ee28 test 2025-02-12 15:55:09 -08:00
Nuno CamposandTat Dat Duong b902db686d Speed up! 2025-02-12 15:55:07 -08:00
Nuno CamposandGitHub 16be48079e fix(sdk-js): improve SSE parsing performance by 10x (#3401) 2025-02-12 10:08:46 -08:00
Eugene YurtsevandGitHub cf3a9ad63e docs: add timeout to docs delpoy workflow (#3402) 2025-02-12 13:04:06 -05:00
David DuongandGitHub 547830ef24 feat(sdk-js): add experimental useStream hook (#3361)
Experimental `useStream` React hook to make streaming as simple as
possible. Supports branching / forking, message-tuple stream mode for
rendering messages.

Usage: https://github.com/langchain-ai/langchain-nextjs-template/pull/62
2025-02-12 09:39:02 -08:00
Tat Dat Duong d333e52fce Simplify types for additional kwargs 2025-02-12 09:16:07 -08:00
Tat Dat Duong 15fe44ddf8 Update deps 2025-02-12 09:13:10 -08:00
Tat Dat Duong b6aff521e5 Make event listeners a callback 2025-02-12 09:11:44 -08:00
Eugene YurtsevandGitHub 35ff9f5211 docs: add script to add typescript translations (#3394)
A script to add typescript translations. We can improve by automatically
adding markdown tabs appropriately and parallelizing the llm calls
2025-02-12 12:07:05 -05:00
Tat Dat Duong d690fc3e00 Bump to 0.0.39 2025-02-12 08:52:25 -08:00
Tat Dat Duong 3325787af7 fix(sdk-js): improve SSE parsing performance by 10x 2025-02-12 08:51:53 -08:00
Tat Dat Duong 77a3cffa7f Allow omitting client 2025-02-12 08:50:01 -08:00
Ben BurnsandGitHub d1f2a6c518 chore(docs): update markdown-exec to end sessions after each page (#3391) 2025-02-11 21:28:30 +00:00
Eugene YurtsevandGitHub b984584851 docs: revert accidental changes to glossary (#3389) 2025-02-11 21:09:03 +00:00
bce4545021 docs: vcr only set up for markdown (#3339)
- PR branch for testing w/ typescript (@benjamincburns )
- implementation needs better cache invalidation for the cassettes
(@eyurtsev)

---------

Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-02-11 15:38:17 -05:00
Vadym BardaandGitHub 29c317887d langgraph: release to 0.2.71 (#3388) 2025-02-11 14:06:04 -05:00
Vadym BardaandGitHub 84a0eca935 langgraph: allow passing destinations to .add_node (#3384) 2025-02-11 19:01:45 +00:00
Tat Dat Duong d9ed1ef52e Branching 2025-02-11 10:10:54 -08:00
Vadym BardaandGitHub fda79e00ce docs: update typo in a tool (#3385)
Fixes #3383
2025-02-11 13:08:33 -05:00
Tat Dat Duong 3163a60466 Fix typo 2025-02-11 09:58:19 -08:00
Tat Dat Duong ac05955222 Update peer dependencies 2025-02-11 09:09:08 -08:00
Tat Dat Duong f3fe30380c Make sure we clear when switching threads 2025-02-11 09:01:53 -08:00
Tat Dat Duong 2c5ddceeb1 Add submit / stop 2025-02-11 09:01:53 -08:00
Tat Dat Duong 98976e016a Prevent double Error: Error 2025-02-11 09:01:52 -08:00
Tat Dat Duong ea8025b719 Add other stream parameters 2025-02-11 09:01:52 -08:00
Tat Dat Duong 735a76a16c Clean up the API 2025-02-11 09:01:52 -08:00
Tat Dat Duong a33437964c Add debugger, that is not exported 2025-02-11 09:01:52 -08:00
Tat Dat Duong cb9405bcee Allow update: null 2025-02-11 09:01:52 -08:00
Tat Dat Duong bd2268404c Update types 2025-02-11 09:01:51 -08:00
Tat Dat Duong 25f88740c1 feat(sdk-js): add experimental useStream hook 2025-02-11 09:01:51 -08:00
David DuongandGitHub e9809ae9c1 feat(sdk-js): strongly-typed messages, state/update-type, stream mode (#3360)
StateType, UpdateType is set on the Client rather than on
`RunsClient.stream` because of lack of partial type arguments
application.

This PR also describes the message serialization format emitted by
LangGraph Server (which may change ie. converting `type` to `role`).
Avoiding direct import of `@langchain/core` for the core LangGraph SDK
client, thus these types were copied from `@langchain/core` (a script is
used to aid with keeping track with core)
2025-02-11 08:56:02 -08:00
Tat Dat Duong 69311a4135 Add feedback stream event 2025-02-11 08:45:15 -08:00
Tat Dat Duong bb3193c83e fix(sdk-js): non-ok response is not throwing anymore 2025-02-11 08:45:15 -08:00
Tat Dat Duong 7a2eb614dc Add ErrorStreamEvent 2025-02-11 08:45:14 -08:00
Tat Dat Duong 7fd6931200 Cleanup types 2025-02-11 08:45:14 -08:00
Tat Dat Duong cbca07e3db Rename to more sane event type 2025-02-11 08:45:14 -08:00
Tat Dat Duong 066525b335 Update content 2025-02-11 08:45:14 -08:00
Tat Dat Duong a1dad43602 feat(sdk-js): strongly-typed messages, state/update-type, stream mode 2025-02-11 08:45:14 -08:00
Vadym BardaandGitHub 1e8f097656 docs: update prebuilt page w/ autogenerated info (#3381) 2025-02-11 09:27:01 -05:00
Brace SproulandGitHub db26c915a9 fix(sdk-js): Export types (#3377) 2025-02-10 18:02:42 -08:00
bracesproul 9b62280fc5 bump dep 2025-02-10 17:47:07 -08:00
bracesproul 515ad8d7a6 fix(sdk-js): Export types 2025-02-10 17:30:19 -08:00
David DuongandGitHub d378f0e06a fix(sdk-js): use type instead of interface to avoid TS errors (#3358)
Allow assigning `threadState?.checkpoint` to the `configurable` object
without TSC throwing "Index signature for type 'string' is missing in
type".
2025-02-10 13:30:25 -08:00
Vadym BardaandGitHub 740870df65 docs: ignore more links in url checker (#3374) 2025-02-10 15:27:58 -05:00
Vadym BardaandGitHub 6e20c9f3f9 docs: update codespell config (#3373) 2025-02-10 14:35:07 -05:00
Vadym BardaandGitHub 530544234a docs: add missing file (#3371) 2025-02-10 12:49:10 -05:00
Vadym BardaandGitHub f11d241482 docs: add adopters page (#3370) 2025-02-10 12:32:14 -05:00
William FHandGitHub e81979827f Support index embed specification via string (#3317)
So you can do

```
from langgraph.store.memory|posgres|etc. import InMemoryStore

InMemoryStore(index={"embed": "openai:text-embedding-3-small"})
```
2025-02-09 02:09:10 +00:00
Tat Dat Duong 2064ea4793 fix(sdk-js): use type instead of interface to avoid TS errors
Allow assigning `threadState?.checkpoint` to the `configurable` object without TSC throwing "Index signature for type 'string' is missing in type".
2025-02-08 10:32:28 -08:00
Brace SproulandGitHub 16cfeff78c release(sdk-js): 0.0.37 (#3354) 2025-02-07 12:44:55 -08:00
bracesproul 4321d337d6 release(sdk-js): 0.0.37 2025-02-07 12:35:51 -08:00
Brace SproulandGitHub d56e2545a6 fix(sdk-js): Remove default fetch timeout (#3353) 2025-02-07 12:34:32 -08:00
bracesproul 9a3f96c459 fix(sdk-js): Remove default fetch timeout 2025-02-07 12:17:34 -08:00
LaelandGitHub cb509ad6a5 Update application_structure.md (#3278)
moved requirements.txt to root of the project in the recommended
structure
2025-02-07 15:14:51 -05:00
Vadym BardaandGitHub e479a2c643 docs: update packages.yml (#3351) 2025-02-07 15:12:00 -05:00
Vadym BardaandGitHub 0caae32a40 docs: expose langgraph-supervisor (#3350) 2025-02-07 14:29:41 -05:00
Vadym BardaandGitHub a6b8098548 cli: release 0.1.71 (#3342) 2025-02-06 17:51:48 -05:00
Vadym BardaandGitHub 0631f19ef9 langgraph: release 0.2.70 (#3341) 2025-02-06 17:50:08 -05:00
Eugene YurtsevandGitHub e9b0bc4d3b cli: patch to convert graphs paths to posix (#3318)
* Convert graph paths to POSIX for docker.
* Will set up windows build in a separate PR (some issue w/ pulling down
the image) https://github.com/langchain-ai/langgraph/pull/3271
* Fix for https://github.com/langchain-ai/langgraph/issues/2061
2025-02-06 17:29:57 -05:00
Vadym BardaandGitHub eb19b80d13 langgraph: add agent name to AI messages in create_react_agent (#3340) 2025-02-06 17:24:53 -05:00
Nuno CamposandGitHub e5ccdd91c2 fix(langgraph): Dedupe input (right-side) messages in add_messages (#3338)
Port: https://github.com/langchain-ai/langgraphjs/pull/846
2025-02-06 13:25:20 -08:00
jacoblee93 871f15a630 Merge branch 'jacob/dedupe' of github.com:langchain-ai/langgraph into jacob/dedupe 2025-02-06 11:52:29 -08:00
jacoblee93 32aa87d4b2 Add test 2025-02-06 11:52:13 -08:00
Nuno Campos 55a3352e9b One more 2025-02-06 11:34:45 -08:00
Nuno Campos 6adaa6cf78 Fix up 2025-02-06 11:33:54 -08:00
Vadym BardaandGitHub 28172a2767 langgraph: allow setting custom names for compiled graph / compiled state graph (#3337) 2025-02-06 19:33:02 +00:00
jacoblee93 028137a51a Format 2025-02-06 11:28:10 -08:00
jacoblee93 c3847fae6c Dedupe input (right-side) messages in add_messages 2025-02-06 11:26:15 -08:00
William FHandGitHub 3daa4ae466 Update name to InMemorySaver (#2044)
(Keep MemorySaver around for backwards compatibility)

"MemorySaver" is ambiguous: is it saving memories? Where is it saving
memories to?

InMemorySaver aligns naming InMemoryStore as well as similar LangChain
objects (InMemoryVectorStore, etc.)
2025-02-05 22:04:25 -08:00
Doros Doru-LucianandGitHub 6a36f4bf91 docs: add forgotten MemorySaver in full code (#3321) 2025-02-05 20:58:17 -05:00
Eugene YurtsevandGitHub 04a0443742 ci: swap order of llms full and site build (#3315) 2025-02-04 22:16:39 +00:00
Eugene YurtsevandGitHub 3b224df566 publish llms text (#3313)
just includes llms-full
2025-02-04 21:56:33 +00:00
Vadym BardaandGitHub 0a2e2b4796 docs: fix typo (#3312) 2025-02-04 16:22:55 -05:00
Eugene YurtsevandGitHub 0e0b3ffac5 Update plans.md (#3311) 2025-02-04 20:45:54 +00:00
Eugene YurtsevandGitHub 75509bd221 docs: generate llms text (#3243)
```shell
python docs/_scripts/generate_llms_text.py llms_text.md
```
2025-02-04 20:30:26 +00:00
William FHandGitHub f997e5eafd Update inv file (#3308)
https://mkdocstrings.github.io/reference/handlers/base/
2025-02-04 11:45:59 -08:00
Eugene YurtsevandGitHub 6337c62dc7 Update prebuilt.md (#3306) 2025-02-04 18:46:57 +00:00
Eugene YurtsevandGitHub 59992fac7c Update create_third_party_page.py (#3307) 2025-02-04 18:44:18 +00:00
Eugene YurtsevandGitHub 28a7ca03a9 docs: publish prebuilt page (#3305)
Publish prebuilt page
2025-02-04 18:26:25 +00:00
TanushreeandGitHub 9fe61a42d2 Adding interrupt banner (#3295) 2025-02-03 18:57:17 -08:00
Marcello MaugeriandGitHub 59ae3b873c Fix typo in quick_start.md (#3297) 2025-02-03 18:51:27 -05:00
Vadym BardaandGitHub 31ba5c41ac docs: use full path for the upload artifact (#3293) 2025-02-03 15:33:50 -05:00
Vadym BardaandGitHub 6ddbdd0638 docs: add vercel build (#3290) 2025-02-03 20:13:58 +00:00
William FHandGitHub 3f7f52ccdc Docs links fix (#3292) 2025-02-03 10:57:38 -08:00
William FHandGitHub ee9a34f726 Update license key troubleshooting (#3157) 2025-02-03 18:09:05 +00:00
Eugene YurtsevandGitHub 72ce34e3e6 docs: add 3rd party packages scaffold (#3263)
* Adds scaffolding for 3rd party packages. Not published yet.
2025-02-03 17:21:30 +00:00
Vadym BardaandGitHub 098d5ec403 langgraph: update RunnableLike to support injected kwargs (#3288)
Fixes #3257
2025-02-03 11:25:20 -05:00
Vadym BardaandGitHub 45ef425d85 docs: update command how-to (#3280) 2025-02-03 09:29:45 -05:00
8c192414a2 docs: Update Hierarchial Multi Agent Example (#3282)
Was trying to learn the Multi Agent Workflow examples and encountered
some errors, which I fixed by editing these:

* Added missing state for Team1, and importing `Command`
* `ValueError: Node `LangGraph` already present.`: Seems to happen we
add the `team_1_graph` node without giving it a name, it will default to
the name `LangGraph`. Solved by giving the sub-graph a name when
building the top-level supervisor.
* Added the edges for the graph to feedback to the top level supervisor
to decide whether it still needs to relegate the task to other nodes or
end from there

---------

Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
2025-02-03 09:29:19 -05:00
Vadym BardaandGitHub c0db7f4d09 docs: revamp streaming how-to guides + update redirects (#3274) 2025-01-31 18:04:51 -05:00
072f2d43eb docs: add graph API basics section (#3228)
![Screenshot 2025-01-30 at 3 53
05 PM](https://github.com/user-attachments/assets/c97018ea-38ff-4b38-96df-55c6d7f926f3)

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
2025-01-31 17:41:56 -05:00
Andrew NguonlyandGitHub c02a74e221 docs: Add Excalidraw file for LangGraph Cloud architecture diagram (#3272) 2025-01-31 14:07:12 -08:00
Vadym BardaandGitHub 15af8a9e6c Revert "docs: revamp streaming how-to guides (#3239)" (#3270)
This reverts commit d5b0ad2cf6.
2025-01-31 15:52:58 -05:00
Vadym BardaandGitHub 1b257b4d0a ci: disable autogen notebook in the runner (#3269) 2025-01-31 15:44:03 -05:00
Vadym BardaandGitHub 384814036e docs: update other frameworks how-to (#3264) 2025-01-31 20:23:41 +00:00
Vadym BardaandGitHub d5b0ad2cf6 docs: revamp streaming how-to guides (#3239) 2025-01-31 15:14:26 -05:00
ccurmeandGitHub 8d8e514924 langgraph[patch]: rename parameter (#3268)
Rename `tool_call_parallelism` (has not been released yet).
2025-01-31 19:36:55 +00:00
a37c4d6f49 langgraph[patch]: allow ToolNode to accept ToolCalls (#3126)
Alternative to https://github.com/langchain-ai/langgraph/pull/3124

Currently if a tool interrupts, the entire tool node executes again
after resuming. So tools can get executed twice if parallel tool calls
are generated. Here we allow ToolNode to accept tool calls, so we can
use the `Send` API to distribute the tool calls to multiple instances of
the tool node.

```python
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command, Send, interrupt


@tool
def human_assistance(query: str) -> str:
    """Request assistance from a human."""
    human_response = interrupt({"query": query})
    return human_response["data"]


@tool
def get_weather(location: str) -> str:
    """Use this tool to get the weather."""
    return "It's sunny!"


tools = [get_weather, human_assistance]
llm = ChatAnthropic(model="claude-3-5-sonnet-20240620")

agent = create_react_agent(
    llm,
    tools,
    checkpointer=MemorySaver(),
    tool_call_parallelism="parallel_tool_nodes",
)


user_input = (
    "Could you please (1) request assistance for building an AI agent "
    "from a human, and (2) search for the weather in Boston, MA? "
    "Generate two tool calls at once."
)

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

for event in agent.stream(
    {"messages": [{"role": "user", "content": user_input}]},
    config,
    stream_mode="values",
):
    event["messages"][-1].pretty_print()
```
```
...
```
```python
human_response = "You should check out LangGraph to build your agent."
human_command = Command(resume={"data": human_response})

for event in agent.stream(human_command, config, stream_mode="values"):
    event["messages"][-1].pretty_print()
```

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
2025-01-31 17:20:59 +00:00
Vadym BardaandGitHub 4b3e07b67a langgraph: release 0.2.69 (#3256) 2025-01-30 19:56:20 -05:00
Nuno CamposandGitHub 6ba61cc768 Guard and cache calls to find_subgraph_pregel (#3255)
- The result of these doesnt change once a node is created, and it's
fairly expensive to run, so great thing to cache
- There's a variety of errors that can come from inspecting the source
code of a function (part of what this does) so adding a catch-all
try-except block as this should be best-effort, not crash your graph
2025-01-30 16:37:42 -08:00
Nuno Campos 5a79210904 Lint 2025-01-30 16:28:03 -08:00
Nuno Campos 4a60eaf32f Guard and cache calls to find_subgraph_pregel
- The result of these doesnt change once a node is created, and it's fairly expensive to run, so great thing to cache
- There's a variety of errors that can come from inspecting the source code of a function (part of what this does) so adding a catch-all try-except block as this should be best-effort, not crash your graph
2025-01-30 16:26:23 -08:00
Vadym BardaandGitHub cf7c3e7fd1 docs: update README to include built w/ langgraph (#3254) 2025-01-30 22:13:06 +00:00
Vadym BardaandGitHub 141b53ee6e docs: add reference file for config (#3253) 2025-01-30 16:58:00 -05:00
37e8e00f1f When using Command.PARENT also pass existing subgraph state to parent graph (#3134)
Co-authored-by: Vadym Barda <vadym@langchain.dev>
2025-01-30 15:54:44 -05:00
Vadym BardaandGitHub a0ec9017f2 langgraph: add get_stream_writer() (#3251)
Alternative to #3250
2025-01-30 20:45:44 +00:00
Eugene YurtsevandGitHub 80cef60405 docs: concepts update (#3248) 2025-01-30 16:32:17 +00:00
Vadym BardaandGitHub 46cba763be docs: small README update (#3244) 2025-01-29 17:46:32 -05:00
Vadym BardaandGitHub 830e5f4550 docs: update README (#3242) 2025-01-29 22:33:32 +00:00
Vadym BardaandGitHub 55e9409b6e docs: update multi-agent multi-turn how-to (#3241) 2025-01-29 22:28:03 +00:00
Vadym BardaandGitHub 39e65a1a62 langgraph: expose tags in the metadata for streamed message chunks (#3238) 2025-01-29 20:24:32 +00:00
Vadym BardaandGitHub 82148e9bf2 docs: update streaming docstring for Pregel and expose in api ref (#3229) 2025-01-29 12:07:07 -05:00
Harsh NevseandGitHub ed09a77d9f Update multi-agent-collaboration.ipynb (#3233)
grammar
2025-01-29 11:48:54 -05:00
William FHandGitHub 7e267897c9 Update auth file paths in build/up (#3231) 2025-01-29 05:09:10 -08:00
Andrew NguonlyandGitHub d279902156 docs: Add LangSmith Integration section for Cloud SaaS concepts (#3230) 2025-01-28 13:27:51 -08:00
Eugene YurtsevandGitHub 440158b969 Update functional_api.md (#3227) 2025-01-28 17:57:01 +00:00
0cf3a64d66 Do not inject args in RunnableCallable if arg already exists (#3185)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-01-28 12:48:33 -05:00
Vadym BardaandGitHub 953e2907d4 docs: remove MessageGraph from concepts doc (#3226) 2025-01-28 10:16:54 -05:00
210 changed files with 24884 additions and 13535 deletions
+38
View File
@@ -20,7 +20,30 @@ env:
POETRY_VERSION: "1.7.1"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
python:
- 'libs/langgraph/**'
- 'libs/sdk-py/**'
- 'libs/cli/**'
- 'libs/checkpoint/**'
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/scheduler-kafka/**'
sdk-js:
- 'libs/sdk-js/**'
lint:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -34,12 +57,14 @@ jobs:
"libs/checkpoint-postgres",
"libs/scheduler-kafka",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
secrets: inherit
test:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -50,6 +75,7 @@ jobs:
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -57,17 +83,23 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
test-scheduler-kafka:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/scheduler-kafka"
uses: ./.github/workflows/_test_scheduler_kafka.yml
secrets: inherit
check-sdk-methods:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
@@ -80,11 +112,15 @@ jobs:
run: python .github/scripts/check_sdk_methods.py
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
lint-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
@@ -109,6 +145,8 @@ jobs:
run: yarn build
test-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
+7 -3
View File
@@ -9,7 +9,11 @@
permissions:
contents: read
defaults:
run:
working-directory: docs
jobs:
codespell:
name: (Check for spelling errors)
@@ -26,13 +30,13 @@
- name: Extract Ignore Words List
run: |
# Use a Python script to extract the ignore words list from pyproject.toml
python .github/workflows/extract_ignored_words_list.py
python ../.github/workflows/extract_ignored_words_list.py
id: extract_ignore_words
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib'
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.md'
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
# We do this to avoid spellchecking cell outputs
- name: Codespell Notebooks
+38 -8
View File
@@ -21,6 +21,10 @@ concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
working-directory: docs
jobs:
get-changed-files:
runs-on: ubuntu-latest
@@ -44,6 +48,7 @@ jobs:
deploy:
# needs: run-changed-notebooks
runs-on: ubuntu-latest
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
@@ -58,26 +63,50 @@ jobs:
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: docs
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
cache: "yarn"
cache-dependency-path: docs/yarn.lock
- name: Install dependencies
run: |
poetry install --with test --no-root
yarn
poetry install --with test --with docs --no-root
poetry run pip install -U \
pytest \
pytest-check-links \
langsmith \
langchain \
GitPython \
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
# we run this installation only for internal PRs
# as GITHUB_TOKEN is not available for PRs from outside contributors
if [ -n "${GITHUB_TOKEN}" ]; then
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
fi
poetry run jupyter kernelspec list
poetry run python3 -m ipykernel install --user --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
- name: Run unit tests
# Run unit tests on the docs build pipeline
run: make tests
- name: Lint Docs
# This step lints the docs using the existing linting set up.
# It should be very fast and should not require any external services.
run: make lint-docs
- name: Build llms-text
run: make llms-text
- name: Build site
run: make build-docs
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
- name: Check links in notebooks
env:
LANGCHAIN_API_KEY: test
@@ -95,14 +124,15 @@ jobs:
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
--check-links-ignore "https://www\.uber\.com/.*" \
--check-links-ignore "https://pepy\.tech/.*" \
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
else
echo "Fetching changes from origin/main..."
git fetch origin main
echo "Checking for changed notebook files..."
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|docs/site/|; s/\.ipynb$/\/index.html/' || true)
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
echo "Changed files: ${CHANGED_FILES}"
if [ -n "${CHANGED_FILES}" ]; then
echo "Running link check on HTML files matching changed notebook files..."
@@ -127,7 +157,7 @@ jobs:
uses: actions/configure-pages@v4
- name: Upload Pages Artifact
if: github.ref == 'refs/heads/main'
# if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: ./docs/site/
@@ -1,6 +1,6 @@
import toml
pyproject_toml = toml.load("libs/langgraph/pyproject.toml")
pyproject_toml = toml.load("pyproject.toml")
# Extract the ignore words list (adjust the key as per your TOML structure)
ignore_words_list = (
+10 -6
View File
@@ -11,6 +11,10 @@ on:
schedule:
- cron: '0 13 * * *'
defaults:
run:
working-directory: docs
jobs:
build:
runs-on: ubuntu-latest
@@ -39,14 +43,14 @@ jobs:
- name: Pre-download tiktoken files
run: |
poetry run python docs/_scripts/download_tiktoken.py
poetry run python _scripts/download_tiktoken.py
- name: Prepare notebooks
run: |
if [ "${{ matrix.lib-version }}" = "development" ]; then
poetry run python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
poetry run python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
else
poetry run python docs/_scripts/prepare_notebooks_for_ci.py
poetry run python _scripts/prepare_notebooks_for_ci.py
fi
- name: Run notebooks
@@ -63,12 +67,12 @@ jobs:
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
echo "Running all notebooks"
./docs/_scripts/execute_notebooks.sh
./_scripts/execute_notebooks.sh
else
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | grep '\.ipynb$' || true)
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | sed 's|^docs/docs/|docs/|' | grep '\.ipynb$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "Running changed notebooks: $CHANGED_FILES"
./docs/_scripts/execute_notebooks.sh $CHANGED_FILES
./_scripts/execute_notebooks.sh $CHANGED_FILES
else
echo "No notebook files changed, skipping execution"
fi
+2 -1
View File
@@ -178,4 +178,5 @@ Untitled*.ipynb
Chinook.db
libs/langgraph/out
.vercel
.turbo
-39
View File
@@ -1,39 +0,0 @@
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc
build-typedoc:
cd libs/sdk-js && yarn install --include-dev && yarn typedoc
cd libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
# Add links to the monorepo
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
build-docs: build-typedoc
poetry run python -m mkdocs build --clean -f docs/mkdocs.yml --strict
serve-clean-docs: clean-docs
poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph
serve-docs: build-typedoc
poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint -w ./libs/sdk-py --dirty
clean-docs:
find ./docs/docs -name "*.ipynb" -type f -delete
rm -rf docs/site
## Run format against the project documentation.
format-docs:
poetry run ruff format docs/docs
poetry run ruff check --fix docs/docs
# Check the docs for linting violations
lint-docs:
poetry run ruff format --check docs/docs
poetry run ruff check docs/docs
codespell:
./docs/codespell_notebooks.sh .
start-services:
docker compose -f docs/test-compose.yml up -V --force-recreate --wait --remove-orphans
stop-services:
docker compose -f docs/test-compose.yml down
+5 -4
View File
@@ -21,10 +21,7 @@ LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [A
### Why use LangGraph?
LangGraph provides fine-grained control over both the flow and state of your
agent applications. It implements a central
[persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/),
enabling features that are common to most agent architectures:
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
@@ -333,6 +330,10 @@ Then we define one normal and one conditional edge. Conditional edge means that
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Resources
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
## Contributing
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
+2
View File
@@ -1,2 +1,4 @@
site/
docs/cloud/reference/sdk/js_ts_sdk_ref.md
.vercel
+66
View File
@@ -0,0 +1,66 @@
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
build-typedoc:
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
# Add links to the monorepo
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
build-prebuilt:
# Use to create an update to date prebuilt page.
# Looks up download stats for each of the prebuilt packages and
# generates the final prebuilt page.
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
build-docs: build-typedoc build-prebuilt
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
poetry run python -m _scripts.generate_llms_text docs/llms-full.txt
install-vercel-deps:
dnf install -y python3.11
curl -sSL https://install.python-poetry.org | python3 -
poetry self update 1.8.5
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
poetry env use /usr/bin/python3.11
poetry install --with docs --with test --no-root
tests:
# Run unit tests
poetry run pytest tests/unit_tests
vercel-build-docs: install-vercel-deps
make build-docs
serve-clean-docs: clean-docs
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
serve-docs: build-typedoc
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
clean-docs:
find ./docs -name "*.ipynb" -type f -delete
rm -rf site
## Run format against the project documentation.
format-docs:
poetry run ruff format docs
poetry run ruff check --fix docs
# Check the docs for linting violations
lint-docs:
poetry run ruff format --check docs
poetry run ruff check docs
codespell:
./codespell_notebooks.sh .
start-services:
docker compose -f test-compose.yml up -V --force-recreate --wait --remove-orphans
stop-services:
docker compose -f test-compose.yml down
+7 -9
View File
@@ -19,23 +19,21 @@ make serve-docs
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
```bash
python docs/_scripts/prepare_notebooks_for_ci.py
./docs/_scripts/execute_notebooks.sh
python _scripts/prepare_notebooks_for_ci.py
./_scripts/execute_notebooks.sh
```
**Note**: if you want to run the notebooks without `%pip install` cells, you can run:
```bash
python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
./docs/_scripts/execute_notebooks.sh
python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
./_scripts/execute_notebooks.sh
```
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
**Note**: this is currently limited only to the notebooks in `docs/docs/how-tos`
## Adding new notebooks
If you are adding a notebook with API requests, it's **recommended** to record network requests so that they can be subsequently replayed. If this is not done, the notebook runner will make API requests every time the notebook is run, which can be costly and slow.
@@ -48,14 +46,14 @@ Then, run
jupyter execute <path_to_notebook>
```
Once the notebook is executed, you should see the new VCR cassettes recorded in `docs/cassettes` directory and discard the updated notebook.
Once the notebook is executed, you should see the new VCR cassettes recorded in `cassettes` directory and discard the updated notebook.
## Updating existing notebooks
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `docs/cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
To delete cassettes for a notebook, you can run:
```bash
rm docs/cassettes/<notebook_name>*
rm cassettes/<notebook_name>*
```
View File
+157
View File
@@ -0,0 +1,157 @@
"""Add typescript translation to a given markdown file."""
import argparse
import re
import requests
from langchain_anthropic import ChatAnthropic
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
response = requests.get(URL)
response.raise_for_status()
reference_snippets = response.text
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
def _get_tqdm():
try:
from tqdm import tqdm
except ImportError:
# If not available return a simple identity function
def tqdm(iterable, *args, **kwargs):
return iterable
return tqdm
_tqdm = _get_tqdm()
opening_pattern = re.compile(r"^\s*```python(?:\s+.*)?\s*$")
closing_pattern = re.compile(r"^\s*```\s*$")
def extract_python_snippets(markdown: str) -> list[str]:
"""
Extract all python code blocks (including their fence lines) from the markdown content.
A python block is defined as any block that starts with a line containing an opening fence
with '```python' (optionally with extra parameters) and ends with a closing fence '```'.
"""
snippets = []
inside_block = False
current_snippet = []
for line in markdown.splitlines(keepends=True):
if not inside_block:
if opening_pattern.match(line):
inside_block = True
current_snippet = [line]
else:
current_snippet.append(line)
if closing_pattern.match(line):
inside_block = False
snippets.append("".join(current_snippet))
current_snippet = []
return snippets
def translate_snippet(python_snippet: str) -> str:
"""Translate a python code block into a TypeScript code block using Langchain.
The response is expected to be a properly fenced TypeScript code block (i.e.
starting with ```typescript and ending with ```).
"""
ai_message = model.invoke(
[
{
"role": "system",
"content": (
f"You have access to the following up-to-date example TypeScript code "
f"snippets that show examples of building with langgraph "
f"and langchain:\n\n{reference_snippets}\n\n"
"Use this context to translate the following Python code to equivalent "
"TypeScript. Ensure that your output is a valid fenced TypeScript "
"code block (i.e. starts with ```typescript and ends with ```)."
),
},
{
"role": "user",
"content": f"Translate this Python snippet to TypeScript:\n\n{python_snippet}",
},
]
)
# Use a regular expression to search for a TypeScript code block in the response.
pattern = r"```typescript\s*(.*?)\s*```"
match = re.search(pattern, ai_message.content, re.DOTALL)
if match:
# Reconstruct the code block with proper fences.
typescript_code = match.group(1).strip()
return f"```typescript\n{typescript_code}\n```"
else:
raise ValueError("No TypeScript code block found in the model's response.")
def insert_translations_into_markdown(
markdown: str, typescript_snippets: list[str]
) -> str:
"""Walks through the original markdown content and, after each
Python snippet block, inserts the corresponding translated TypeScript snippet.
It assumes that the ordering of the Python snippets
(from extract_python_snippets) matches the order they appear in the markdown.
"""
output_lines = []
lines = markdown.splitlines(keepends=True)
inside_block = False
snippet_index = 0
for line in lines:
output_lines.append(line)
if not inside_block and opening_pattern.match(line):
# We've encountered the start of a python code block.
inside_block = True
elif inside_block:
if closing_pattern.match(line):
# End of a python snippet block.
inside_block = False
if snippet_index < len(typescript_snippets):
# Insert an extra newline for clarity, then the translated TypeScript snippet.
output_lines.append("\n")
output_lines.append(typescript_snippets[snippet_index])
output_lines.append("\n")
snippet_index += 1
return "".join(output_lines)
def main(file_path: str) -> None:
# Read the markdown file.
with open(file_path, "r") as f:
markdown_content = f.read()
# 1. Extract all Python snippets.
python_snippets = extract_python_snippets(markdown_content)[:1]
# 2. Translate each Python snippet to TypeScript.
typescript_snippets = []
# Replace with .batch() for faster translation
for python_snippet in _tqdm(python_snippets):
ts_snippet = translate_snippet(python_snippet)
typescript_snippets.append(ts_snippet)
# 3. Insert the TypeScript translations after their respective Python snippets.
updated_markdown = insert_translations_into_markdown(
markdown_content, typescript_snippets
)
# Overwrite the original markdown file with the updated content.
with open(file_path, "w") as f:
f.write(updated_markdown)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Translate Python snippets in a markdown file to TypeScript and insert them after each Python snippet."
)
parser.add_argument("file_path", type=str, help="Path to the markdown file.")
args = parser.parse_args()
main(args.file_path)
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Read the list of notebooks to skip from the JSON file
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('docs/notebooks_no_execution.json'))))")
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('notebooks_no_execution.json'))))")
# Function to execute a single notebook
execute_notebook() {
@@ -27,7 +27,7 @@ if [ $# -gt 0 ]; then
notebooks=$(echo "$@" | tr ' ' '\n' | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
else
# Find all notebooks and filter out those in the skip list
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
notebooks=$(find docs/tutorials docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
fi
# Execute notebooks sequentially
+121 -145
View File
@@ -1,9 +1,9 @@
import ast
import importlib
import inspect
import logging
import re
from functools import lru_cache
from typing import List, Literal, Optional
from typing import List, Optional
from typing_extensions import TypedDict
@@ -39,7 +39,6 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
([], "langgraph.types", "StreamMode", "types"),
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
(["langgraph.constants"], "langgraph.types", "Send", "types"),
@@ -48,7 +47,9 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.constants"], "langgraph.types", "Command", "types"),
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
(["langgraph.func"], "langgraph.func", "task", "func"),
([], "langgraph.types", "RetryPolicy", "types"),
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
@@ -68,34 +69,19 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
}
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
if not pkg_prefix.isidentifier():
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
return re.compile(
r"from\s+(" + pkg_prefix + "(?:_\w+)?(?:\.\w+)*?)\s+import\s+"
r"((?:\w+(?:,\s*)?)*" # Match zero or more words separated by a comma+optional ws
r"(?:\s*\(.*?\))?)", # Match optional parentheses block
re.DOTALL, # Match newlines as well
)
# Regular expression to match langchain import lines
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
try:
module = importlib.import_module(module_path)
class_ = getattr(module, class_name)
module = inspect.getmodule(class_)
if module is None:
# For constants, inspect.getmodule() might return None
# In this case, we'll return the original module_path
symbol = getattr(module, class_name)
# First check the __module__ attribute on the symbol.
mod_name = getattr(symbol, "__module__", None)
# If __module__ is not set or comes from typing,
# assume the definition is in module_path.
if mod_name is None or mod_name.startswith("typing"):
return module_path
return module.__name__
return mod_name
except AttributeError as e:
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
return None
@@ -104,139 +90,128 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
return None
def _get_doc_title(data: str, file_name: str) -> str:
try:
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
except IndexError:
pass
# Parse the rst-style titles
try:
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
except IndexError:
return file_name
class ImportInformation(TypedDict):
imported: str # The name of the class that was imported.
source: str # The full module path from which the class was imported.
docs: str # The URL pointing to the class's documentation.
title: str # The title of the document where the import is used.
path: str # The path of the file where the markdown content originated.
def _get_imports(
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
) -> List[ImportInformation]:
"""Get imports from the given code block.
Args:
code: Python code block from which to extract imports
doc_title: Title of the document
package_ecosystem: "langchain" or "langgraph". The two live in different
repositories and have separate documentation sites.
Returns:
List of import information for the given code block
"""
imports = []
if package_ecosystem == "langchain":
pattern = _IMPORT_LANGCHAIN_RE
elif package_ecosystem == "langgraph":
pattern = _IMPORT_LANGGRAPH_RE
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
for import_match in pattern.finditer(code):
module = import_match.group(1)
if "pydantic_v1" in module:
continue
imports_str = (
import_match.group(2).replace("(\n", "").replace("\n)", "")
) # Handle newlines within parentheses
# remove any newline and spaces, then split by comma
imported_classes = [
imp.strip()
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
if imp.strip()
]
for class_name in imported_classes:
module_path = _get_full_module_name(module, class_name)
if not module_path:
continue
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"title": doc_title,
}
)
return imports
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
def get_imports(code: str, path: str) -> List[ImportInformation]:
"""Retrieve all import references from the given code for specified ecosystems.
Args:
code: The source code from which to extract import references.
doc_title: The documentation title associated with the code.
path: The path of the file where the markdown content originated.
Returns:
A list of import information for each import found.
"""
ecosystems = ["langchain", "langgraph"]
all_imports = []
for package_ecosystem in ecosystems:
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
return all_imports
# Parse the code into an AST.
try:
tree = ast.parse(code)
except SyntaxError:
return []
found_imports = []
# Walk through the AST and process ImportFrom nodes.
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
# node.module is the source module.
if node.module is None:
continue
for alias in node.names:
if not (
node.module.startswith("langchain")
or node.module.startswith("langgraph")
):
continue
found_imports.append(
{
"source": node.module,
# alias.name is the original name even if an alias exists.
"imported": alias.name,
}
)
imports: list[ImportInformation] = []
for found_import in found_imports:
module = found_import["source"]
if module.startswith("langchain"):
# Handles things like `langchain` or `langchain_anthropic`
package_ecosystem = "langchain"
elif module.startswith("langgraph"):
package_ecosystem = "langgraph"
else:
continue
class_name = found_import["imported"]
module_path = _get_full_module_name(module, class_name)
if not module_path:
continue
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"path": path,
}
)
return imports
def update_markdown_with_imports(markdown: str) -> str:
def update_markdown_with_imports(markdown: str, path: str) -> str:
"""Update markdown to include API reference links for imports in Python code blocks.
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
This function scans the markdown content for Python code blocks, extracts any
imports, and appends links to their API documentation.
Args:
markdown: The markdown content to process.
path: The path of the file where the markdown content originated.
Returns:
Updated markdown with API reference links appended to Python code blocks.
@@ -247,10 +222,12 @@ def update_markdown_with_imports(markdown: str) -> str:
```python
from langchain.nlp import TextGenerator
```
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
This function will append an API reference link to the `TextGenerator` class
from the `langchain.nlp` module if it's recognized.
"""
code_block_pattern = re.compile(
r'(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```', re.DOTALL
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
re.DOTALL,
)
def replace_code_block(match: re.Match) -> str:
@@ -262,9 +239,8 @@ def update_markdown_with_imports(markdown: str) -> str:
Returns:
str: The modified code block with API reference links appended if applicable.
"""
indent = match.group('indent')
code_block = match.group('code')
language = match.group('language') # Preserve the language from the regex match
indent = match.group("indent")
code_block = match.group("code")
# Retrieve import information from the code block
imports = get_imports(code_block, "__unused__")
@@ -274,11 +250,11 @@ def update_markdown_with_imports(markdown: str) -> str:
return original_code_block
# Generate API reference links for each import
api_links = ' | '.join(
api_links = " | ".join(
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
)
# Return the code block with appended API reference links
return f'{original_code_block}\n\n{indent}API Reference: {api_links}'
return f"{original_code_block}\n\n{indent}API Reference: {api_links}"
# Apply the replace_code_block function to all matches in the markdown
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
+90
View File
@@ -0,0 +1,90 @@
"""Experimental script to generate consolidated llms text from the docs."""
import glob
import os
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from _scripts.notebook_hooks import _on_page_markdown_with_config
HERE = os.path.dirname(os.path.abspath(__file__))
# Get source directory (parent of HERE / docs)
SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(HERE), "docs"))
def _make_llms_text(output_file: str) -> str:
"""Generate a consolidated text file from markdown/notebook files for LLM training.
Args:
output_file: Path to output the consolidated text file
"""
# Collect all markdown and notebook files
relative_paths = [
# Files relative to docs/docs/
"tutorials/introduction.ipynb",
]
all_files = [os.path.join(SOURCE_DIR, path) for path in relative_paths]
all_files.extend(
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.md"), recursive=True)
)
all_files.extend(
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.ipynb"), recursive=True)
)
# Add all concepts
all_files.extend(
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.md"), recursive=True)
)
all_files.extend(
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.ipynb"), recursive=True)
)
all_content = []
# Process each file
for file_path in all_files:
print(f"Processing {file_path}")
rel_path = os.path.relpath(file_path, SOURCE_DIR)
# Create File and Page objects to match mkdocs structure
file_obj = File(
path=rel_path, src_dir=SOURCE_DIR, dest_dir="", use_directory_urls=True
)
page = Page(
title="",
file=file_obj,
config={},
)
# Read raw content
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Convert to markdown without logic to resolve API references
processed_content = _on_page_markdown_with_config(
content, page, add_api_references=False, remove_base64_images=True
)
if processed_content:
# Add file name
all_content.append(f"---\n{rel_path}\n---")
# Add content
all_content.append(processed_content)
# Write consolidated output
with open(output_file, "w", encoding="utf-8") as f:
f.write("\n\n".join(all_content))
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description=(
"Generate consolidated text file from markdown/notebook files for LLMs."
)
)
parser.add_argument("output_file", help="Path to output the consolidated text file")
args = parser.parse_args()
_make_llms_text(args.output_file)
+247 -7
View File
@@ -1,28 +1,266 @@
import ast
import os
import re
from pathlib import Path
from typing import Literal
import nbformat
from nbconvert.exporters import MarkdownExporter
from nbconvert.preprocessors import Preprocessor
def _uses_input(source: str) -> bool:
"""Parse the source code to determine if it uses the input() function."""
try:
tree = ast.parse(source)
except SyntaxError:
# If there's a syntax error, assume input() might be present to be safe.
return False
for node in ast.walk(tree):
if isinstance(node, ast.Call):
# Check if the function called is named 'input'
if isinstance(node.func, ast.Name) and node.func.id == "input":
return True
return False
def _rewrite_cell_magic(code: str) -> str:
"""Process a code block that uses cell magic.:w
- Lines starting with "%%capture" are ignored.
- Lines starting with "%pip" are rewritten by removing the leading "%" character.
- Any other non-empty line causes a NotImplementedError.
Args:
code (str): The original code block.
Returns:
str: The transformed code block.
Raises:
NotImplementedError: If a line doesn't start with either "%%capture" or "%pip".
"""
rewritten_lines = []
for line in code.splitlines():
stripped = line.strip()
# Skip empty lines
if not stripped:
continue
# Ignore %%capture lines
if stripped.startswith("%%capture"):
continue
# Rewrite %pip lines by dropping the '%'
elif stripped.startswith("%pip"):
# Drop the leading '%' character
rewritten_lines.append(stripped[1:])
# Anything else is not supported
else:
raise NotImplementedError(f"Unhandled line: {line}")
return "\n".join(rewritten_lines)
class PrintCallVisitor(ast.NodeVisitor):
"""
This visitor sets self.has_print to True if it encounters a call
to a print within the global scope.
This should catch calls to print(), print_stream(), etc. (Prefixed with "print").
May have some false positives, but it's not meant to be perfect.
Temporary code for notebook conversion.
"""
def __init__(self):
self.has_print = False
self.scope_level = 0 # counter to track whether we're inside a def/lambda
def visit_FunctionDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_AsyncFunctionDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_Lambda(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_ClassDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_Call(self, node):
# Only consider calls when not inside a function definition.
if self.scope_level == 0:
if isinstance(node.func, ast.Name) and node.func.id.startswith("print"):
self.has_print = True
self.generic_visit(node)
def _has_output(source: str) -> bool:
"""Determine if the code block is expected to produce output.
Args:
source (str): The source code of the code block.
Returns:
True if the code block is expected to produce output, False otherwise.
Must meet the following conditions:
1. There is a call to a printing function (name starts with "print")
that is not inside a function definition.
2. The last top-level statement is an expression that is valid if:
- It is any expression (including calls) AND
- It is NOT a call to `display(...)`.
`display` isn't handled currently by markdown-exec
"""
try:
tree = ast.parse(source)
except SyntaxError:
return False
# Condition (1): Check for a global print-like call.
visitor = PrintCallVisitor()
visitor.visit(tree)
condition_a = visitor.has_print
# Condition (2): Check the last top-level statement.
condition_b = False
if tree.body:
last_stmt = tree.body[-1]
if isinstance(last_stmt, ast.Expr):
# If the expression is a call, ensure it's not a call to "display"
if isinstance(last_stmt.value, ast.Call):
if (
isinstance(last_stmt.value.func, ast.Name)
and last_stmt.value.func.id == "display"
):
condition_b = False # exclude display-wrapped expressions
else:
condition_b = True
else:
# Any other expression qualifies.
condition_b = True
return condition_a or condition_b
def _convert_links_in_markdown(markdown: str) -> str:
"""Convert links present in notebook markdown cells to standardized format.
We want to update markdown links code cells by linking to markdown
files rather than assuming that the link is to the finalized HTML.
This code is needed temporarily since the markdown links that are present
in ipython notebooks do not follow the same conventions as regular markdown
files in mkdocs (which should link to a .md file).
"""
# Define the regex pattern in parts for clarity:
pattern = (
r"(?<!!)" # Negative lookbehind: ensure the link is not an image (i.e., doesn't start with "!")
r"\[" # Literal '[' indicating the start of the link text.
r"(?P<text>[^\]]*)" # Named group 'text': match any characters except ']', representing the link text.
r"\]" # Literal ']' indicating the end of the link text.
r"\(" # Literal '(' indicating the start of the URL.
r"(?![^\)]*//)" # Negative lookahead: ensure that the URL does not contain '//' (skip absolute URLs).
r"(?P<url>[^)]*)" # Named group 'url': match any characters except ')', representing the URL.
r"\)" # Literal ')' indicating the end of the URL.
)
def custom_replacement(match):
"""logic will correct the link format used in ipython notebooks
Ipython notebooks were being converted directly into HTML links
instead of markdown links that retain the markdown extension.
It needs to handle the following cases:
- optional fragments (e.g., `#section`)
e.g., `[text](url/#section)` -> `[text](url.md#section)`
e.g., `[text](url#section)` -> `[text](url.md#section)`
- relative paths (e.g., `../path/to/file`) need to be denested by 1 level
"""
text = match.group("text")
url = match.group("url")
if url.startswith("../"):
# we strip the "../" from the start of the URL
# We only need to denest one level.
url = url[3:]
url = url.rstrip("/") # Strip `/` from the end of the URL
# if url has a fragment
if "#" in url:
url, fragment = url.split("#")
url = url.rstrip("/")
# Strip `/` from the end of the URL
return f"[{text}]({url}.md#{fragment})"
# Otherwise add the .md extension
return f"[{text}]({url}.md)"
return re.sub(
pattern,
custom_replacement,
markdown,
)
class EscapePreprocessor(Preprocessor):
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
self.markdown_exec_migration = markdown_exec_migration
def preprocess_cell(self, cell, resources, cell_index):
if cell.cell_type == "markdown":
# rewrite markdown links to html links (excluding image links)
cell.source = re.sub(
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
r'<a href="\2">\1</a>',
cell.source,
)
if not self.markdown_exec_migration:
# Old logic is to convert ipynb links to HTML links
cell.source = re.sub(
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
r'<a href="\2">\1</a>',
cell.source,
)
else:
cell.source = _convert_links_in_markdown(cell.source)
# Fix image paths in <img> tags
cell.source = re.sub(
r'<img\s+src="\.?/img/([^"]+)"', r'<img src="../img/\1"', cell.source
)
elif cell.cell_type == "code":
# Determine if the cell has bash or cell magic
source = cell.source
is_exec = not (
source.startswith("%") or source.startswith("!") or _uses_input(source)
)
cell.metadata["exec"] = is_exec
if self.markdown_exec_migration:
# For markdown exec migration we'll re-write cell magic as bash commands
if source.startswith("%%"):
cell.source = _rewrite_cell_magic(source)
cell.metadata["language"] = "shell"
cell.metadata["has_output"] = _has_output(source)
# Remove noqa comments
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
# escape ``` in code
# This is needed because the markdown exporter will wrap code blocks in
# triple backticks, which will break the markdown output if the code block
# contains triple backticks.
cell.source = cell.source.replace("```", r"\`\`\`")
# escape ``` in output
if "outputs" in cell:
@@ -115,9 +353,11 @@ exporter = MarkdownExporter(
def convert_notebook(
notebook_path: Path,
) -> Path:
mode: Literal["markdown", "exec"] = "markdown",
) -> str:
with open(notebook_path) as f:
nb = nbformat.read(f, as_version=4)
nb.metadata.mode = mode
body, _ = exporter.from_notebook_node(nb)
return body
+154 -19
View File
@@ -1,13 +1,14 @@
import logging
import os
import posixpath
import re
from typing import Any, Dict
from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from notebook_convert import convert_notebook
from generate_api_reference_links import update_markdown_with_imports
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.notebook_convert import convert_notebook
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -15,6 +16,24 @@ logger.setLevel(logging.INFO)
DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
REDIRECT_MAP = {
# lib redirects
"how-tos/stream-values.ipynb": "how-tos/streaming.ipynb#values",
"how-tos/stream-updates.ipynb": "how-tos/streaming.ipynb#updates",
"how-tos/streaming-content.ipynb": "how-tos/streaming.ipynb#custom",
"how-tos/stream-multiple.ipynb": "how-tos/streaming.ipynb#multiple",
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming-tokens.ipynb#example-without-langchain",
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
# cloud redirects
"cloud/index.md": "concepts/index.md#langgraph-platform",
"cloud/how-tos/index.md": "how-tos/index.md#langgraph-platform",
"cloud/concepts/api.md": "concepts/langgraph_server.md",
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
}
class NotebookFile(File):
def is_documentation_page(self):
return True
@@ -38,6 +57,29 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
return new_files
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
"""Add the path to the code blocks."""
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
r"(?P=indent)```" # Match closing backticks with the same indentation
)
def replace_code_block_header(match: re.Match) -> str:
indent = match.group("indent")
language = match.group("language")
attributes = match.group("attributes").rstrip()
if 'exec="on"' not in attributes:
# Return original code block
return match.group(0)
code = match.group("code")
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
return code_block_pattern.sub(replace_code_block_header, markdown)
def _highlight_code_blocks(markdown: str) -> str:
"""Find code blocks with highlight comments and add hl_lines attribute.
@@ -52,7 +94,7 @@ def _highlight_code_blocks(markdown: str) -> str:
# existing hl_lines for Python and JavaScript
# Pattern to find code blocks with highlight comments, handling optional indentation
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
r"(?P=indent)```" # Match closing backticks with the same indentation
)
@@ -61,6 +103,13 @@ def _highlight_code_blocks(markdown: str) -> str:
indent = match.group("indent")
language = match.group("language")
code_block = match.group("code")
attributes = match.group("attributes").rstrip()
# Account for a case where hl_lines is manually specified
if "hl_lines" in attributes:
# Return original code block
return match.group(0)
lines = code_block.split("\n")
highlighted_lines = []
@@ -86,35 +135,121 @@ def _highlight_code_blocks(markdown: str) -> str:
# Reconstruct the new code block
new_code_block = "\n".join(lines_to_keep)
# Construct the full code block that also includes
# the fenced code block syntax.
opening_fence = f"```{language}"
if attributes:
opening_fence += f" {attributes}"
if highlighted_lines:
return (
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
# The indent and terminating \n is already included in the code block
f'{new_code_block}'
f'{indent}```'
)
else:
return (
f"{indent}```{language}\n"
# The indent and terminating \n is already included in the code block
f"{new_code_block}"
f"{indent}```"
)
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
return (
# The indent and opening fence
f"{indent}{opening_fence}\n"
# The indent and terminating \n is already included in the code block
f"{new_code_block}"
f"{indent}```"
)
# Replace all code blocks in the markdown
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
return markdown
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
def _on_page_markdown_with_config(
markdown: str,
page: Page,
*,
add_api_references: bool = True,
remove_base64_images: bool = False,
**kwargs: Any,
) -> str:
if DISABLED:
return markdown
if page.file.src_path.endswith(".ipynb"):
logger.info("Processing Jupyter notebook: %s", page.file.src_path)
# logger.info("Processing Jupyter notebook: %s", page.file.src_path)
markdown = convert_notebook(page.file.abs_src_path)
# Append API reference links to code blocks
markdown = update_markdown_with_imports(markdown)
if add_api_references:
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
# Apply highlight comments to code blocks
markdown = _highlight_code_blocks(markdown)
# Add file path as an attribute to code blocks that are executable.
# This file path is used to associate fixtures with the executable code
# which can be used in CI to test the docs without making network requests.
markdown = _add_path_to_code_blocks(markdown, page)
if remove_base64_images:
# Remove base64 encoded images from markdown
markdown = re.sub(r"!\[.*?\]\(data:image/+;base64,[^\)]+\)", "", markdown)
return markdown
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
return _on_page_markdown_with_config(
markdown,
page,
add_api_references=True,
**kwargs,
)
# redirects
HTML_TEMPLATE = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<link rel="canonical" href="{url}">
<meta name="robots" content="noindex">
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
<meta http-equiv="refresh" content="0; url={url}">
</head>
<body>
Redirecting...
</body>
</html>
"""
def write_html(site_dir, old_path, new_path):
"""Write an HTML file in the site_dir with a meta redirect to the new page"""
# Determine all relevant paths
old_path_abs = os.path.join(site_dir, old_path)
old_dir_abs = os.path.dirname(old_path_abs)
# Create parent directories if they don't exist
if not os.path.exists(old_dir_abs):
os.makedirs(old_dir_abs)
# Write the HTML redirect file in place of the old file
content = HTML_TEMPLATE.format(url=new_path)
with open(old_path_abs, "w", encoding="utf-8") as f:
f.write(content)
# Create HTML files for redirects after site dir has been built
def on_post_build(config):
use_directory_urls = config.get("use_directory_urls")
for page_old, page_new in REDIRECT_MAP.items():
page_old = page_old.replace(".ipynb", ".md")
page_new = page_new.replace(".ipynb", ".md")
page_new_before_hash, hash, suffix = page_new.partition("#")
old_html_path = File(page_old, "", "", use_directory_urls).dest_path.replace(
os.sep, "/"
)
new_html_path = File(page_new_before_hash, "", "", True).url
new_html_path = (
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
+ hash
+ suffix
)
write_html(config["site_dir"], old_html_path, new_html_path)
+23 -22
View File
@@ -7,7 +7,7 @@ import click
import nbformat
logger = logging.getLogger(__name__)
NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
NOTEBOOK_DIRS = ("docs/how-tos","docs/tutorials")
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")
@@ -19,36 +19,37 @@ BLOCKLIST_COMMANDS = (
)
NOTEBOOKS_NO_CASSETTES = (
"docs/docs/how-tos/visualization.ipynb",
"docs/docs/how-tos/many-tools.ipynb"
"docs/how-tos/visualization.ipynb",
"docs/how-tos/many-tools.ipynb"
)
NOTEBOOKS_NO_EXECUTION = [
# this uses a user provided project name for langsmith
"docs/docs/tutorials/tnt-llm/tnt-llm.ipynb",
"docs/tutorials/tnt-llm/tnt-llm.ipynb",
# this uses langsmith datasets
"docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
"docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
# this uses browser APIs
"docs/docs/tutorials/web-navigation/web_voyager.ipynb",
"docs/tutorials/web-navigation/web_voyager.ipynb",
# these RAG guides use an ollama model
"docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
"docs/docs/tutorials/rag/langgraph_crag_local.ipynb",
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
"docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
"docs/tutorials/rag/langgraph_crag_local.ipynb",
"docs/tutorials/rag/langgraph_self_rag_local.ipynb",
# this loads a massive dataset from gcp
"docs/docs/tutorials/usaco/usaco.ipynb",
"docs/tutorials/usaco/usaco.ipynb",
# TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
"docs/docs/how-tos/autogen-integration.ipynb",
"docs/how-tos/autogen-integration.ipynb",
"docs/how-tos/autogen-integration-functional.ipynb",
# TODO: need to update these notebooks to make sure they are runnable in CI
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
"docs/docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
"docs/docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
"docs/docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
"docs/docs/tutorials/tot/tot.ipynb",
"docs/docs/how-tos/visualization.ipynb",
"docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb"
"docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
"docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
"docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
"docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
"docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
"docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
"docs/tutorials/tot/tot.ipynb",
"docs/how-tos/visualization.ipynb",
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
]
@@ -216,7 +217,7 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
except Exception as e:
logger.error(f"Error processing {notebook_path}: {e}")
with open(os.path.join(DOCS_PATH, "notebooks_no_execution.json"), "w") as f:
with open("notebooks_no_execution.json", "w") as f:
json.dump(NOTEBOOKS_NO_EXECUTION, f)
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python
"""Create the third party page for the documentation."""
import argparse
from typing import List
from typing import TypedDict
import yaml
MARKDOWN = """\
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
# 🚀 Prebuilt Agents
LangGraph includes a prebuilt React agent. For more information on how to use it,
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
If youre looking for other prebuilt libraries, explore the community-built options
below. These libraries can extend LangGraph's functionality in various ways.
## 📚 Available Libraries
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
{library_list}
## ✨ Contributing Your Library
Have you built an awesome open-source library using LangGraph? We'd love to feature
your project on the official LangGraph documentation pages! 🏆
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml]({langgraph_url}) file.
**Guidelines**
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
for JavaScript/TypeScript, etc.) 📦
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
the Functional API (exposing an `entrypoint`).
- The package must include documentation (e.g., a `README.md` or docs site)
explaining how to use it.
We'll review your contribution and merge it in!
Thanks for contributing! 🚀
"""
class ResolvedPackage(TypedDict):
name: str
"""The name of the package."""
repo: str
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
weekly_downloads: int | None
"""The weekly download count of the package."""
description: str
"""A brief description of what the package does."""
def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -> str:
"""Generate the markdown content for the third party page.
Args:
resolved_packages: A list of resolved package information.
language: str
Returns:
The markdown content as a string.
"""
# Update the URL to the actual file once the initial version is merged
if language == "python":
langgraph_url = (
"https://github.com/langchain-ai/langgraph/blob/main/docs"
"/_scripts/third_party_page/packages.yml"
)
elif language == "js":
langgraph_url = (
"https://github.com/langchain-ai/langgraphjs/blob/main/docs"
"/_scripts/third_party/packages.yml"
)
else:
raise ValueError(f"Invalid language '{language}'. Expected 'python' or 'js'.")
sorted_packages = sorted(
resolved_packages, key=lambda p: p["weekly_downloads"] or 0, reverse=True
)
rows = [
"| Name | GitHub URL | Description | Weekly Downloads |",
"| --- | --- | --- | --- |",
]
for package in sorted_packages:
name = f"**{package['name']}**"
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
downloads = package["weekly_downloads"] or "-"
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
rows.append(row)
markdown_content = MARKDOWN.format(
library_list="\n".join(rows), langgraph_url=langgraph_url
)
return markdown_content
def main(input_file: str, output_file: str, language: str) -> None:
"""Main function to create the third party page.
Args:
input_file: Path to the input YAML file containing resolved package information.
output_file: Path to the output file for the third party page.
language: The language for which to generate the third party page.
"""
# Parse the input YAML file
with open(input_file, "r") as f:
resolved_packages: List[ResolvedPackage] = yaml.safe_load(f)
markdown_content = generate_markdown(resolved_packages, language)
# Write the markdown content to the output file
with open(output_file, "w", encoding="utf-8") as f:
f.write(markdown_content)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Create the third party page.")
parser.add_argument(
"input_file",
help="Path to the input YAML file containing resolved package information.",
)
parser.add_argument(
"output_file", help="Path to the output file for the third party page."
)
parser.add_argument(
"--language",
choices=["python", "js"],
default="python",
help="The language for which to generate the third party page. Defaults to 'python'.",
)
args = parser.parse_args()
main(args.input_file, args.output_file, args.language)
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python
"""Retrieve download count for a list of Python packages from PyPI."""
import argparse
from datetime import datetime
from typing import TypedDict
import pathlib
import requests
import yaml
class Package(TypedDict):
"""A TypedDict representing a package"""
name: str
"""The name of the package."""
repo: str
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
description: str
"""A brief description of what the package does."""
class ResolvedPackage(Package):
weekly_downloads: int | None
HERE = pathlib.Path(__file__).parent
PACKAGES_FILE = HERE / "packages.yml"
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
resolved_packages: list[ResolvedPackage] = []
for package in packages:
# First check if package exists on PyPI
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
try:
pypi_response = requests.get(pypi_url)
pypi_response.raise_for_status()
except requests.exceptions.HTTPError:
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
# Get first release date
pypi_data = pypi_response.json()
releases = pypi_data["releases"]
first_release_date = None
for version_releases in releases.values():
if version_releases: # Some versions may be empty lists
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
if first_release_date is None or upload_time < first_release_date:
first_release_date = upload_time
if first_release_date is None:
raise AssertionError(f"Package {package['name']} has no releases yet")
# If package was published in last 48 hours, skip download stats
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
response = requests.get(url)
response.raise_for_status()
data = response.json()
sorted_data = sorted(
data["data"],
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
reverse=True,
)
# Sum the last 7 days of downloads
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
else:
num_downloads = None
resolved_packages.append(
{
"name": package["name"],
"repo": package["repo"],
"weekly_downloads": num_downloads,
"description": package["description"],
}
)
return resolved_packages
def main(output_file: str) -> None:
"""Main function to generate package download information.
Args:
output_file: Path to the output YAML file.
"""
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES)
if not output_file.endswith(".yml"):
raise ValueError("Output file must have a .yml extension")
with open(output_file, "w") as f:
f.write("# This file is auto-generated. Do not edit.\n")
yaml.dump(resolved_packages, f)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate package download information."
)
parser.add_argument(
"output_file",
help=(
"Path to the output YAML file. Example: python generate_downloads.py "
"downloads.yml"
),
)
args = parser.parse_args()
main(args.output_file)
@@ -0,0 +1,17 @@
#A list of third-party packages to surface on the third-party page.
packages:
- name: "trustcall"
repo: "hinthornw/trustcall"
description: "Tenacious tool calling built on LangGraph"
- name: "breeze-agent"
repo: "andrestorres123/breeze-agent"
description: "A streamlined research system built inspired on STORM and built on LangGraph"
- name: "langgraph-supervisor"
repo: "langchain-ai/langgraph-supervisor"
description: "Build supervisor multi-agent systems with LangGraph"
- name: "langmem"
repo: "langchain-ai/langmem"
description: "Build agents that learn and adapt from interactions over time."
- name: "langchain-mcp-adapters"
repo: "langchain-ai/langchain-mcp-adapters"
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
eNrtGUtv48Y52566pwZo7wRRoEAhyqSot+GDLTu2d2PLtrT2rhNDGA2HIm1yhuYMZckLHbLpvWDQP9CsI6eGs0mQoE3Spuceit69h/yI/oJ8Q0lrOfbGm2SDJpAFQRQ13/vNT4+O2yTkLqO3Tl0qSIiwgBv+zqPjkOxHhIs/9n0iHGYdrVVr9cdR6J79wREi4OWpKRS4aRYQitw0Zv5U25jCDhJT8D3wSELmqMms7tNbrYeqTzhHLcLVsvLGQzVkHoFvasRJqKYUFTPgTYX86QBI/J4rLlWEQ5QmsULGfLW3A1A+s4gnYVqB0Mx0ThNR2GQSn4uQIB+ORBgRuBeMeUNWohskrOyIJqpJ8Gffy8pDlSI/AWgR0XAF8bmEsAjHoRsMgdR7nIA4LlckYfhQPMb2lChQDhwXO0qCpqCQjMRuuW1ClcBDmKQluQCFwAXMyxOWI5lYc5dgkQCEYMhQuGQAkGBeAAUNXdpSez0Alp5xQ2JJBYegO71eb6d37BBkAZM/HTmMi/jJRQd9iDAmYDlCMbOAWPxB69ANUopFbA8JcgJOoCSxS3yyR0igIQ/U6A+w4o9QEHguRvJ8apczejp0miZFvHx8In2rgcupiD+f5V2KqyDJ7PLUWhfCiSpGOltM6x91NC6QSz0ID81DIFQ/SM7/MX4QILwHlLRhqMb9AfKTcRjG4/dWEK7WLpBEIXbi91Do57OfjP8eRlS4PomPK2uX2Q0Pz9mZacNIlz6+QFhqFH+QXMrJp8v+foEIEWFXwwxoxX/Rn4yM5RHaEk782MyV3g8JDyBJyNt9QBMRf3QEjiH/+ffxMFnerd4defSrV357NA9Oir+sO1FKyeSVGgmUjJ7JKoZZzsq3srhSP60M2dSlT84UQTpiirTlL4MUmVYgRUNOxEwkbK34cT1ElNvgqIVRUBxjJ6J7xDqpXBkOX8pwAPWkPpDFGukEjBNtKGZ8el/bGNQNbXn+k0HsaSxsIeoeJrER/1V6GYRw6afDY4h9SRKYaz6PH2eymSfDk5EDTkBRXTN0TTc+l4mAIeik4AELQTGCoSqJbnyW8lFHRtyMaeTMvK7r05CP2IssUoua88wHnnxaCULiMWR90dFCMKjn+i54JfkcVjwIJAOQ9c8uQwi2RyiP38/pg9e/xkFCIjlINZ4ROirB659XA41oZSVMqVD44iIY+OiczuO8zz+7fD4k8a7OTzsjYM214rPfwU2jVLJLmWyuqBvZnNksZQyUMzMoj4hRKOaJaX9YeU2rIOwQrZaEX3w8/2B1dmW5clID2hWocC555+mtXzYa2G40/Zkt74Fr8s1Wox7ou5m2u7gxZ/Olzfn6HO8Yr7f9ppPLpdOr9vZ9rBmFTMHMFTMZUzPSetpIG1recMPD0kp1gRv39l+fx0v59U26udVebdJNI4PTa8JaQAW2d2fznrUdLBp364vtdQd7peDQQ12y7S1FS3ZtLXKKfrViNxcXzUYJr4M/kXBmpqYViEQoinxmmCAaJIgm0yNbNkbpMa1YSRTMpC9WxmllCVpalXrdacgrCCcCVyjYNSjsM6uMkrM/gw2itmvN4NqcjvT9/XzVt+nC3kaHNnfX1zfq1BGdg+2D2vycMGsru41wdn3MCLpZ1PSRHfRsMQmec9G/p1R/u6+N57tWTfoV+JEyTl3b7tdICCkUn2CPRRYU+ZD0wecbsw/iT4u4ZCIb3k0L42zW1Ba2NpJu/VZ/0Gqe/uqRhQSSPciFTqPK1o6hsWuzc7vZ9fVwkdYP767o+VVat7fszYM7lfbiAVJTo6Y2wEifDwPppLAAAIZCJGT3emafTGrU2y+2dsj3TA4weJdDh23YIBYJA5AOyNPI84CWw1wsmyZ0epdapKOW9RR0b08gtTyaM1QE1QVKLKClzkeNAQHZ0BsYed43aQyUhoMGn6XNzY3tpVX3TnHX3pqbe5AtvLa87wKxQXcemy3GRovRZDE+WKgobEU+sAduKjTznRQ0czviyBvI00upHmtBQWzykYCgtMudBtiMS6oJ1E7v9u2fv3Oea/pxG44b7OGbic1ujPRtRhrMgzdmusZMb6rlm2i63kzw/HVjpOuMNHhCvbHSdRnXmzQjXa/ruBnHdH5jvrq6sHP79svcwvyi/8O3MCnlHGtsqLqAqg73L6PgkJykB5XrpqkX2cuMBZQi54Gk2yW1fChjEmVSgm+ufWTkKWNh23hBmUbqSkR5f6VYY+pzhvd4SuEOI3CxIi6UZkSp3OzcLLG+yxLrq1d+fbPG+omtsfo42QrEZ//7iS8FfoTH9UsrvHyh+N1WeL+ZlBWeYRoTuMLLl176Ci/fzJQyum6TbMFCBdPCmWJWz+tG0SgWs6ade+4K7yWshggq4uzVq6FX//v80WwDVxe3816xUqwuNfWVlVknbK9XrCj//UYz8/+xGoIZ5kfYy/xcLHNuhmUYmyZTczkATazu5+P+ROqfmljHd1k0sbo7qD25GZ88tN7k+8S5XS4pbtw+aW5H1JpY3eVCbnIHu0nVPKIT+yTjTm6FT0+U4i/wZxMXLLjqb6avAWQIP1w=
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
eNrtWUFz28YVjtube2gP7R2D6alDQABBACQ1OkiUZMuVREmkHFmWhrMEFgREAAthF5QojQ51e+8g0z+QWJFSjeIkk0ybpE3PPfTYi3zIP8ilv6BvQVIiK9tKMs4kKcUDCWDfvn3f997ue3h8ctrBMfVIeOfcCxmOkcXghr715DTGuwmm7A8nAWYusY9XqrX60yT2Ln7jMhbR8sQEijyZRDhEnmyRYKKjTlguYhNwHfk4U3PcJHb3+Z3tQzHAlKIWpmJZeHwoWgTWChnciHswRfCowFws7GEEP7HghQJ1xJwgxsTHXCihOBaPtuFJQGzs80etiEmarEssiZuEy4bwVIVfymKMArhhcYLhnuEgAlwgx1UpssmfEeL3TWHdKFvCScIMOld1eV0WDsUQBZkAxSi2XD5sY2rFXtSXECvI9wVGBJrETh9GU+ZyEYphLpBKM0VRDGTFzMO9W2A37mZXAxPAci9siUdHHDmw78XY5kb2RTn8gShp7mCLgejR9tGpi5ENi/zx2CWUpc9G3fIBsiwMXOHQIjaoT99vHXhRTrCx4yOGz8AVIc7QpmdtjCMJ+V4Hn/RmpR+iKPI9C/HxiR1KwvO+6yRuyfXhM+4pCRwdsvSzadoNrSpYMr0wsdKFIAoFVS4UZeXDfYky5IU+BIXkIzDqJMrG/zY8ECGrDZqkfoCmJ73Jz4ZlCE3fXUJWtTaikjsqfRfFgVH4ePh5nITMC3B6Wlm5vlx/8Go5TVZVufTRiGKOKH0/+yln3x7564gSzOKuZBHQlb6tPBuQ5eOwxdz0qVYovRdjGsHWwL8/gWksoU+OwTH4X/887W+Rd6q/HXj0yzd+dTwLTkq/qLtJTsgbQg1HQl7JFwRVK2tmWSkJ95bq55X+MnXukwuB4X02gTv8SW8zTAqwMWOK2VTCHKn4UT1GIXXAUXODoDi13CRsY/us8sJw+IKHA8DjeGBPSng/IhRLfTPT8w1prXdaSAuzH/diTyJxC4XeQRYb6Z+5l8EIL/ykPwy7gauExaWApk/zqv6sPzJwwBkAVSRVkRT1M741LAg6bnhEYgCGLTiLWDe9yAVon0fclKbqmqEoyiScH5af2LiWNGdJAGvSSSGKsU+Q/fm+BEcB9r3AA69k3/1zDgJJhcnKp9clGGnjkKbv6Urv849hkRjzFTiMS0XHJfj8/cVCA10FLlMy9c9HxcBHV3qeGgH99Pp4X8U7Cj3fHwhLnp1e/BpuGnnb1LBZUNS8biLHaJYKdt5wFL1QLBnNoq1/UJmXKshysVTLwi89nX20PL20UDmrge4KIW0Pv/X8zk8bDctpNIOpaONAtoukTb25B7src3OO3+gqb2rF6vx6frGzM63O1Hc2F5UHTr0tqWbe1HSzmC9JqqzIqqxKrc6qe1/Rgkpjfaf6oBVtmmshmw1mq/kWRNxaqbLa9vfmDI+tzy9uajVnYbpgtlG7PTNn3ivsuS1TPug0Xa+1R3T2aNmQ26sVPT8N/oRMMTUxKUAkwjFJp/obRIINIvHtUSgrg+0xKdhZFEzJoyfjpHAfElk19LuTsK8gnDD8woFd8xieWiYhvvgTcJB0PHuq4dOFanvVbxobu/pGfb1SNPzF/CbaXVyrdjc2cbWz1FyJO1ZiLg2RUMyrktLnwVAKxSx4rkz/llb9ZUMa3u9SNUtE4MeQ0NBznJMajmELpWeWTxIbDvkYn4DP16YfpZ8UrZKGdKQaSDXVkmFJc2+uZTn6dye95PP8Z/+2EUM8K3mQe0Se0C1I59L0jNfVE9M8eLjbraz6syWddvbrwd598BkSc4OE1JshX5UAcnawgIAFBxHj+eyKn9wgm48mc4nHLsygXQq5u+GAWTiOwDpQHya+D7pc4lk8jUL+9kIb74tlJQdp2WdILB/2qwYRwekCRyxMy10VHD0FPP83LMjb/6ujBxoGGlWycW+e6ZvgouW6szkTLc9vBKsPZ0BZLwkPVQxDBcOgXrgsF0QUt5IA1oalREjY2znI7U5Ckd8z5ign+qQFp2GTDqwDxB51G0AY5Sozqe2ju3d//J55Ke/DBA4TdriVcXZL0qtI6pWHtzTdQNOWWL6Npptp6r+E3RJ1E1FQZt6SdCNJNXTL0s0szcOLjeVRi9xydfMxfjRuJN2MdZjGIcyPZ6vLc9t3777Obt5P3n493bycMDwzQ3UlMlS7CyMh8rKWHfetcFPlfmNnbyjOBF57ZpUV1A3CljiEBo60qw17GY4jcMTHW2KFv391hT2PuQLiHZjQwgJxBBd5vrwlbg9TwiGOQG18HUC3HdFXd0S/fOMXtz3RH1hP9MTKWkzpxX9+4B2m76D3c60fbJjaN+sH//LV/WBV+X/pB6uKOob9YMN47f1g1FRVzVILStEuqCWtUMKGVsSmmi/qtm44xkv7wa+hz6ibtmO9uM/4869eXpUZM7q5GUw799bbheX70Yqm0ofNINmd+1ZVmaZ8H31GUfwu+nw/FmauaKi7WBxT6MJlJ2VM8fP+yJhCz7oeY4p9qJcxroFPxxY6VGExXPrd8WUge+0f36TnMXdswaPxjfqsvTW28IkzttB5O3NcwctjBfxr/A1BGYle9AfEfwFXqGb4
eNrtnU1z28YZgJvpzT+gh55QTE8dgQIIfkEaTYeiZJuyKcqiJFr+GM0SWBAwASyMXVCkPD407bkdZvoHGjtSo3GcZJJpk7TpuYf+AefQ39IXFGVJY1frRrST2i8P/AKw2H323fdj8WLx/kGfJtxn0XtP/EjQhNgCfvAP3j9I6P2UcvG7/ZAKjzmP15qtjUdp4j/7lSdEzOdmZ0ns51hMI+LnbBbO9o1Z2yNiFr7HAR0X87jDnOGz6IEaUs5Jl3J1Trn9QLUZnCoS8ENtJ76gClHusR58dFgqFJsIrs4oasICmu2ScpqoD+/CPyFzaJD91Y2FVmBa6Ed+ticXCSUhbBBJSh8eeJQ40Kg/PPYYF6OnZ6v5KbFtCofTyGaOH3VHn3T3/HhGcagbEEEPoW4RHUMYHfYojTUS+H26f3TU6DMSx4EPNYTts/c4i55M2qKJYUxf3HyYVV6Dlkdi9HWVDyO7CTWp1mfXhgA1Uoxc0cqVPxtoXBA/CoCSFhCo1H483v630xtiYvegJG3SYaP9o4Ofnt6H8dFHDWI3W2eKJIntjT4iSVgqfHH6/ySNhB/S0UFt7cXTTTaenM7MGUbO+vxMwVmLRp+MP+bG7z7765lCqEiGms2grNGf9KfHsAIadYU3emQY+p8TymMQFfrbfThMpPz9x9Ax9F//PJjIzIfNa8c9+u+f/OzxEnTS6Ns2dWaUvKWskEjJ6/miYlhzZnGuUFCuNDae1Can2cj65Jki6EDM0n72z5GkzCsgqAmnYiEVrlb5fCMhEXeho5aPheLA9tKoR53D2kvF4dtMHKB5WXtATDU6iBmn2qSaoyc3tfWj0aPVl744kj2NJV0S+Xtj2Rh9nPUyVMKPvpxsjhOWFQkn10I+epQ38k8nW4474BAaqmuGrunGNwMtAQ6BH/oAc/w+Ga7Q/6YOr69e3EPAEIv46GOjqB+9/nF6n4SGUJvs9Ccl5S14/f3lez0vrZDtZJVL35zdD+ielPQoH/KvXtw+KeNDnT8ZHO+s+c7o2S/hx45ll0t2yTTKdqdj5U1acs2SYVpULxcrNqHG19CZvg2lZL0XswR6l9qgoMRw9GwmJINs2C2YRtEsQVvnFT+yg9ShrbSzxLJG8HklTmjAiPNp7bJWI7ZHtdZYAkcHS9ur1Ua9dtiCStYY6/n0g+/e++nOju3udMKF3Ua3lW6t3GitDLZuirazWWlWB4NSwi+TJbqy2ee+ZXZWQtqvL2tG2awYZcssFDQjp+eMnKHdZ+0b6fXFIejavYK15CVJ5ZqRdKxrEYtL/PKV/q51i+419EZtozqs3Lu/3S6X2tFS2/N7g+3LV3upyXv1jUVzZfEq7xXK26VuunO9C60hwluYnVdAGH3guzAZIxqMEQ1GSF6f049HyLzijBks5M4qx3nlKuj2ZhQM55VWBpPCJwlpC5T0wiqL6LM/AoO07zsL8aJzfam7dL3d7plrXrUVupuX091yjtuiV22mtcaSZ1dXr66v6fwUBEsvaPqEQ0kvVMZyeFL171mrv9zUTg95rRmPrc/oIGI88l13v0UTGEWjQztgqQN6PqH70Ofr1e3Rl5ZudQixy2Vq6AXTcrXl9voBCUCY+vboC89cUIGZqc4rIVmolAq6PrZpv9nPhC/qfvfzuw4RZE55oPqOOqdmBtAG86dV+5UKrXeu5Is3mjVWbDR0wrteXtTzeee+OqOyzj1QK5MjcicmMzdWPLCDDYpKUCjzBN7MsQE8bf800ArAtKwZFTgKrE3ft+mO8MFmzqlg1EgaiGzDkAsa7rhQZ5rEUPXs3G68U85Tp0w6xYKdndNjcDAYabDRfuTQgTqnz0AhgSDq3IOJQVYJaClQ1VFW7HNLrsKPhLopJ1C/KA2ChzNqwLqg1Tr86I8ZFU7uc28HGgaWcbLX3YeXLr07BE9wtb2hiojORaQ40FpkdD4j4VFkJGEE/jgykjDiPjKSMQICiAjV0YXVEVQ4FdmcBoI6D9SvEZBEku5Ed1AlSSAtUpukHLWSTJbQ/ksR7ULMSzEkkfoADBFJEGUzyQhJAomgbZMhokM0bBi1YdT2BhiFDL1IKaVfIKCXAJIzUblg4BCcULm91Fxdvnvp0jTTMMQrpGFwjyVCiRkNMRnj3UvGKE03GaP41iRjmJX/r2SMwtSTMSolxzbMUsmyK/DmlDod6pZN3chXnEKhUq78YMkY9eV7nSuc7W3sbZntbWoN3K1iP+zvdvKb1+iNmpk6rLSY1FLd7Z3kIRSfJ2N4mxvr166bm/Hydn9QjVbWGjc3+w33esmuWoOiWbzW0lv33c2wvbLVrhFD1G+tr7U3B8teg3t73Wqp3Q2CW429fq3rxv62VY9zi25p91WTMYpTSsaobtrN6pXw8pYg1ZvLBcvruDy+Vb9lbjSCmzdcfzV/lQ429nbs2qmMlGK5bP04kzFsQvOFgqlb/2Myxu8//+8+hWuksbdp9KyqWfcXm9urpdLK1vqwFn8/n6L47iVjvG0ET3DVpxQivr2EFJ4iIwmjYFpT6G+zGMUM4gmkJLnQ4NGEIiWZLHnEYbsoTTJOcUCGCOl8SDMISCJFyh10ACSMWsxFD0Bu23we0wTVtjT7mUQ2OgGot38s1/beYkbjixwISZKOgbZNGpHsoqctg5RDQOhpX/juOV94yEiWHNanCaoj6ewIEjqfEEsxDnmVZFUHGUkYdRNiUzcNEJQEVEAJqiWM/NGPfN2MNjyKHpJ0xpaSPjoA8nm2hKVdjEqk09pZzifOtqF1u6AcdRhOAbwCpADDEgzdpqC1p3bDKs5sv9MRCcYk8pXhkND5hHzeQ0TnI8IEm1eI2HZ9YeNgw0jk4osw4GCTOtldRCS5fYTtIiLURTjn/7qztP0g+4KUzpckqKRALxJV0jQSkPEa2yusCYuX2HCidgo5NgwR4TQtOpG4YsQP7kD6jkMRkwwTEpLlH7MeRiEYhWAU8gaikDTB5GyMQqZg1DB/HVU2hiGvP1ckIXFMUR9JH26CkoTrREzjATBJiFnH8icuuIhI5kQiIekVkT0M2KQ3iqKfjdP9mJX9Zu4U9ROEhItpXIxQmqAQSRDhZL/U6vs9jNWkz+tKuW8jJZyEvOCSfowJD8pCUChJF10cklJMO0Yxwqsir5tRFQHJdNEwREbnM4o9FuEEJE71XzwPkpIE7b6EUofiEuzoHF387gf4DHBuBFPYLs7IDTBcw4trGK69fkZNzKhBw39Rbe3buCaUbIERdIykYpT4NHIwEMF82ouLUopX+jEOmdJDM5ASOkgYh+Blox/6/geW4HrieE1kGoIEm9E/QjcbH3L4ZiiFHXQjcTp7Cm4kEnoJITkTlQsWq6eo3F5qri7fvXTpP8g9u4o=
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
eNrtnc1T3MgVwHcrN065JFW5KaqcUmiQ5nuGohK+soY1DJhvfxTVklqjBkktpNYwg8uHOLmntJV/IGsvZCmvd7e8lXg32ZxzyD/AHvZ/2PwFeRoGA2XHTTxAMH4cGBi1Wq1fv37vdffT08O9Fo1ixoP3n7BA0IhYAv6JP3q4F9GthMbi97s+FS63H881FhYfJRE7+KUrRBjXh4ZIyHI8pAFhOYv7Qy1jyHKJGIK/Q492q3lscrtzENxXfRrHpEljta7cua9G3KPwl5rENFIHFdXicOlAZF+tRExQhSgbfBM+TJ4IxSIiVh/cg3I+t6mXlWqGQityzWcBy86PRUSJDwdElNAHey4lNtzUHx67PBbp09PN/JxYFoXTaWBxmwXN9LPmDgsHFZs6HhF0H9oS0C6EdH+T0lAjHmvR3cOz0i9IGHoMWgTHhzZiHjzptV0TnZC+fHg/u0UN7jwQ6dejcSewGtCS0amhuQ5ADRQjV6rlKl+0tVgQFnhASfMINGo37B7/28kDIbE2oSat12Hp7uHJT0+W4XH6yQyxGgunqiSR5aafkMgvF5+d/D5KAsF8mu6Nz718ud7B48sVcoaRq315quLsjtLPuh/17m/G/3qqEiqijmZxqCv9k/70CJZHg6Zw00eGof85onEIokJ/twuniSR++Bg6hv7rn3s9mfm48eFRj37/3k8fT0Anpd+uUHtQydeUaRIoeT1fUoxavVCql4rKBzOLT8Z7l1nM+uRAEbQthmgr++ZQUoYVENQopmIkEY5W/XIxIkHsQEdNHgnFnuUmwSa198dfKQ7fZuIAt5fdDwizRtshj6nWa2b6ZFW7dTh6tKmJZ4eyp/GoSQK205WN9NOsl6ERLPiqdziMeFYlXFzz4/RRvmo87R056oB9uFFdM3RNN75paxFw8JjPAGb3d2+4Qv8XdPh5/nIJAUMqiNNPjZJ++POPk2Ui6kNrsssf15Svwc/fX13qRW3FrFCtUv7mdDmge1zTo7wfP3/5eK+Oj/X4SfuosMbs9OAX8M86LRRMs1S1C6V82agUi9V83nKccq1QdgzbtsyvoTOZBbVkvRfyCHqXWqCgRCc9GPRJOxt2IwWjVCjDvQ4rLLC8xKYLiTnBs5uIh5Uwoh4n9ufjv9HGieVSbaErgenexNrs6MzU+P4CNHKc801GP/ru/R+tr1vOuumPmGKMLPrLo9Nu4+aNpVV7tT2+VVtanJ+3F0JRs5NyMLXV3loudlZnNKNSqBqVWqFU1IycnjNyhrY0b/Jmsjy/Zt6u5efXSWVH50s7SVKJt2fL1ZtL7cVqUPZbca1RWd6ZKnbak3PTt0uG7wT5CeoXrbGx2+2txdVybXLGNLxqu1kb23Tn4W6IcEeGhhUQRgZ8R3pjRIMxosEIyet1/WiEDCt2l8FI7rRyHFZugG5vBF5nWFnIYFL4JD5dAKU8MssDevBHYJC0mD2ythRuTC1Pbmzs2DvLs3S0mA+8lblpcyyaW6Mrq4252dKNuU576sMbaycgFPSSpvc4lPVitSuHx01/w1b9ZVU7OeS1Rti1PulewOOAOc7uAo1gFKX7lscTG/R8RHehz2+NrqVf1fSaSUzdMInhGNWarU2u3NojHghTy0qfuYURtV4sFtRhxScj1XJR17s27be7mfAFze9+9mubCFJX7qvMVutqZgAtMH/aaKtaDcbntqcnVmZWtttzSzdbrery9E1ScmuJOqhycwPUSu+M3LHJzHUVDxSwQFEJCnW+gFcYPDKAJ+2fBlqhqOkVzajCWWBtWsyi64KBZa2rYNRI4onsQCcW1F93oM00CqHp2bWdcL2Sp3aFmKWilV3T5XAyGGmw0SywaVut64NQiSeIWj8y2yoBLQWqOsiqfWG5Vfgnok4SE2hfkHjeg0HV403QamZ8+MWgChdnsbsONwaWsVfq3oOBgXeH4DGuFbejIqLXIlK2SYyMJIyES5GRhBH448hIwihmQkBVyEnCCQggIlRJfaskaHAi4BoI6rWgfnU3uIsjTgJpjFokiXHQyQYdQzdA7nIHWeMQk8TEcUQkQZQtliIkCSSCtk2GiHbQsOGkBCcll8DI5+hFSin9HAG9ApCciRoLDg7BMZU7E43ZyXsDA+cZaSDeINIgdnkklJBTv894g+/f+zFGHFyxiINdq7t7mx78+4pv3l7Atuoroi3K/1u0xU/elWgLo5Z/u6ItiucebVElVWLYxQIxK5VSpVItOXaF1px83imVino5f37RFpajWd1oi/h0tMU57OKXSbVkv8Eu/vMf/rul5ltld9rhHzhjN5uJV9hYmm2Mlactvfpmlrp4EZbatKuFQr7mlAtXcRf/uhE8xjV1ThOv60sI3Cti8+0YOck4eZRuIqXXUxpEQPJ1jg5Cko017ggPMUllCVpnIyVUSf2JkXIXvSRZNCgTLjKS7U27LA5pRFElyUg1I2JRpCTdfEVJku6++jQiHnKSu93oK6GvhL7SJTBadNG0yXfyOcqRBJLHmq5ASBJJskmQFURMlxOBdb1XJlmElCSUnAQZoReJXuRFMxpFmyZ1IpkdoI90lu0kCu0TKFBSToJEKE2X9WzRtRYk9CRlkHIISO4loZ90htU2DJSQ6yOw/xbu4OKOW/+McLRJCJFtVNq4QtKvJkJCEkIRbRIPKUnTZuECCaojXLC9eEZzCEjmF+EKpNTso66+tEQZ19o3IjbO09Dq9ylGLiURbhudISTCw4mINADJQUQSRLiZjVob52r4ONsVkaPQIx0wbchJxgkJvZ4QTzBEGw0bpo64FEjQ6hgXSHAR6RyerOnmkERKqLVxOnLBsf4ISJZag0f4kD+usZ2HIAVZTlbkJHsVDSptaeqRFo1wPiJN9eOhpy0LjEAvG58VwWdFLmHZHxf9JYRYjCl+JYjQezzD8to2w8eN5IPtvF5njMtr7/AspINba/KwWhxosmRjfBsRoS7CpX58Icv/fQeb7+CymhxSFOBM5AxbIpiHRRrlz5o4VcPMEP0zQh8SfUj0ITFc5ApsOsIn7jti5Hq/hJII3Ue0aJjK5xJed4iQpG86MvGtWaiP0MPGVVp8W8Zb42FHNKYBam3U2n2PN+ajFEkzscBh3BORxvZjTp+zJD9ArY1h2RiWfQmUGrj9iO5Rv5afeixAhS3FFDGKLhKOt74FyeSYQUv+sDHDVCwYWXMugoTZD1Bn48I/Lvxj2oO3KHU2UkKV3efL1niCjGRhx5S00M0+gyThdq08eSY62qi10dHGGPar8Oa+rMGISZpfjGB8DepsjNK+cEKbAd/GlDVnedk6iVAhoUJCJxJXa/FFB2/H2zI9TMGKycXPIQECLkSi4cc1/8tQ2ayJxl8eNoJBWvKQSI/gg2wY6d/3dA0JvYKQnIkaCx6qJ6jcmWjMTt4bGPgPVCH+Og==
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
+12 -1
View File
@@ -1,6 +1,17 @@
ERROR_FOUND=0
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
# Adding regexp to ignore base64 strings
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
if [ -n "$OUTPUT" ]; then
echo "Errors found in $file"
echo "$OUTPUT"
ERROR_FOUND=1
fi
done
for file in $(find $1 -name "*.md"); do
# Adding regexp to ignore base64 strings
OUTPUT=$(cat "$file" | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
if [ -n "$OUTPUT" ]; then
echo "Errors found in $file"
echo "$OUTPUT"
+25
View File
@@ -0,0 +1,25 @@
# 🦜🕸️ LangGraph Adopters
This list of companies using LangGraph and their success stories is compiled from public sources. If your company uses LangGraph, we'd love for you to share your story and add it to the list. Youre also welcome to contribute updates based on publicly available information from other companies, such as blog posts or press releases.
| Company | Industry | Use case | Reference |
| --- | --- | --- | --- |
| [AirTop](https://www.airtop.ai/) | Software & Technology (GenAI Native) | Browser automation for AI agents | [Case study, 2024](https://blog.langchain.dev/customers-airtop/) |
| [AppFolio](https://www.appfolio.com/) | Real Estate | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-appfolio/) |
| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) |
| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) |
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) |
| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) |
| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) |
| [OpenRecovery](https://www.openrecovery.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-openrecovery/) |
| [Rakuten](https://www.rakuten.com/) | E-commerce / Fintech | Copilot for domain-specific task | [Blog post, 2025](https://rakuten.today/blog/from-ai-hype-to-real-world-tools-rakuten-teams-up-with-langchain.html) |
| [Replit](https://replit.com/) | Software & Technology | Code generation | [Blog post, 2024](https://blog.langchain.dev/customers-replit/); [Breakout agent story, 2024](https://www.langchain.com/breakoutagents/replit); [Fireside chat video, 2024](https://www.youtube.com/watch?v=ViykMqljjxU) |
| [Rexera](https://www.rexera.com/) | Real Estate (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-rexera/) |
| [Tradestack](https://www.tradestack.uk/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-tradestack/) |
| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) |
| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) |
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

@@ -0,0 +1,15 @@
# Prompt Engineering in LangGraph Studio
In LangGraph Studio you can iterate on the prompts used within your graph by utilizing the LangSmith Playground. To do so:
1. Open an existing thread or create a new one.
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
![Playground in Studio](../img/studio_playground.png){width=1200}
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
+2 -2
View File
@@ -99,7 +99,7 @@ We can stream the results of a stateless run in an almost identical fashion to h
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--url <DEPLOYMENT_URL>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
@@ -144,7 +144,7 @@ In addition to streaming, you can also wait for a stateless result by using the
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/runs/runs/wait \
--url <DEPLOYMENT_URL>/runs/wait \
--header 'Content-Type: application/json' \
--data '{
"assistant_id": <ASSISTANT_IDD>,
+417
View File
@@ -0,0 +1,417 @@
# How to integrate LangGraph into your React application
!!! info "Prerequisites"
- [LangGraph Platform](../../concepts/langgraph_platform.md)
- [LangGraph Server](../../concepts/langgraph_server.md)
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
Key features:
- Messages streaming: Handle a stream of message chunks to form a complete message
- Automatic state management for messages, loading states, and errors
- Conversation branching: Create alternate conversation paths from any point in the chat history
- UI-agnostic design - bring your own components and styling
Let's explore how to use `useStream()` in your React application.
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
## Installation
```bash
npm install @langchain/langgraph-sdk @langchain/langchain-core react
```
## Example
```tsx
"use client";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";
export default function App() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [{ type: "human", content: message }] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
Stop
</button>
) : (
<button key="submit" type="submit">
Send
</button>
)}
</form>
</div>
);
}
```
## Customizing Your UI
The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
- Thread state management
- Loading and error states
- Message handling and updates
- Branching support
Here are some examples on how to use these features effectively:
### Loading States
The `isLoading` property tells you when a stream is active, enabling you to:
- Show a loading indicator
- Disable input fields during processing
- Display a cancel button
```tsx
export default function App() {
const { isLoading, stop } = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<form>
{isLoading && (
<button key="stop" type="button" onClick={() => stop()}>
Stop
</button>
)}
</form>
);
}
```
### Thread Management
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
```tsx
const [threadId, setThreadId] = useState<string | null>(null);
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId: threadId,
onThreadId: setThreadId,
});
```
We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
### Messages Handling
To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
```tsx
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
export default function HomePage() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
);
}
```
### Branching Support
To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
A branch can be created in following ways:
1. Edit a previous user message.
2. Request a regeneration of a previous assistant message.
```tsx
/* eslint-disable @typescript-eslint/no-floating-promises */
"use client";
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
import { useState } from "react";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
});
function BranchSwitcher({
branch,
branchOptions,
onSelect,
}: {
branch: string | undefined;
branchOptions: string[] | undefined;
onSelect: (branch: string) => void;
}) {
if (!branchOptions || !branch) return null;
const index = branchOptions.indexOf(branch);
return (
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const prevBranch = branchOptions[index - 1];
if (!prevBranch) return;
onSelect(prevBranch);
}}
>
Prev
</button>
<span>
{index + 1} / {branchOptions.length}
</span>
<button
type="button"
onClick={() => {
const nextBranch = branchOptions[index + 1];
if (!nextBranch) return;
onSelect(nextBranch);
}}
>
Next
</button>
</div>
);
}
function EditMessage({
message,
onEdit,
}: {
message: Message;
onEdit: (message: Message) => void;
}) {
const [editing, setEditing] = useState(false);
if (!editing) {
return (
<button type="button" onClick={() => setEditing(true)}>
Edit
</button>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const content = new FormData(form).get("content") as string;
form.reset();
onEdit({ type: "human", content });
setEditing(false);
}}
>
<input name="content" defaultValue={message.content as string} />
<button type="submit">Save</button>
</form>
);
}
export default function App() {
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => {
const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
return (
<div key={message.id}>
<div>{message.content as string}</div>
{message.type === "human" && (
<EditMessage
message={message}
onEdit={(message) =>
thread.submit(
{ messages: [message] },
{ checkpoint: parentCheckpoint }
)
}
/>
)}
{message.type === "ai" && (
<button
type="button"
onClick={() =>
thread.submit(undefined, { checkpoint: parentCheckpoint })
}
>
<span>Regenerate</span>
</button>
)}
<BranchSwitcher
branch={meta?.branch}
branchOptions={meta?.branchOptions}
onSelect={(branch) => thread.setBranch(branch)}
/>
</div>
);
})}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [message] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
Stop
</button>
) : (
<button key="submit" type="submit">
Send
</button>
)}
</form>
</div>
);
}
```
### TypeScript
The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
- State shape
- Update format
- Custom events
```tsx
// Define your types
type State = {
messages: Message[];
context?: Record<string, unknown>;
};
type Update = {
messages: Message[] | Message;
context?: Record<string, unknown>;
};
type CustomEvent = {
type: "progress" | "debug";
payload: unknown;
};
// Use them with the hook
const thread = useStream<State, Update, CustomEvent>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
If you're using LangGraph.js, you can reuse your graph's annotation types:
```tsx
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
context: Annotation<string>(),
});
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
## Event Handling
The `useStream()` hook provides several callback options to help you respond to different events:
- `onError`: Called when an error occurs.
- `onFinish`: Called when the stream is finished.
- `onUpdateEvent`: Called when an update event is received.
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
- `onMetadataEvent`: Called when a metadata event is received.
## Learn More
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
+1 -1
View File
@@ -90,7 +90,7 @@ For this guide, we'll use the pre-built Python [**ReAct Agent**](https://github.
</figure>
## Lagraph Studio Web UI
## LangGraph Studio Web UI
Once your application is deployed, you can test it in **LangGraph Studio**.
+1 -1
View File
@@ -34,10 +34,10 @@ Below are examples of directory structures for Python and JavaScript application
│ │ ├── 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
├── requirements.txt # package dependencies
└── langgraph.json # configuration file for LangGraph
```
=== "Python (pyproject.toml)"
+7
View File
@@ -27,12 +27,19 @@ LangGraph Platform provides different security defaults:
- Requires valid API key in `x-api-key` header
- Can be customized with your auth handler
!!! note "Custom auth"
Custom auth **is supported** for all plans in LangGraph Cloud.
### Self-Hosted
- No default authentication
- Complete flexibility to implement your security model
- You control all aspects of authentication and authorization
!!! note "Custom auth"
Custom auth is supported for **Enterprise** self-hosted plans.
Self-hosted lite plans do not support custom auth natively.
## System Architecture
A typical authentication setup involves three main components:
+2 -2
View File
@@ -83,12 +83,12 @@ node at a time or if you want to pause the graph execution at specific nodes.
### `NodeInterrupt` exception
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
We recommend that you [**use the `interrupt` function instead**][langgraph.types.interrupt] of the `NodeInterrupt` exception if you're trying to implement
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
??? node "`NodeInterrupt` exception"
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of _dynamic breakpoints_ is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
```python
def my_node(state: State) -> State:
+3 -3
View File
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
@@ -49,9 +49,9 @@ For more information, please see:
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
With a Self-Hosted Lite deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
+152
View File
@@ -0,0 +1,152 @@
# Durable Execution
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
!!! tip
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
## Requirements
To leverage durable execution in LangGraph, you need to:
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
## Determinism and Consistent Replay
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
## Using tasks in nodes
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
=== "Original"
```python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
url: str
result: NotRequired[str]
def call_api(state: State):
"""Example node that makes an API request."""
# highlight-next-line
result = requests.get(state['url']).text[:100] # Side-effect
return {
"result": result
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"url": "https://www.example.com"}, config)
```
=== "With task"
```python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import task
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
urls: list[str]
result: NotRequired[list[str]]
@task
def _make_request(url: str):
"""Make a request."""
# highlight-next-line
return requests.get(url).text[:100]
def call_api(state: State):
"""Example node that makes an API request."""
# highlight-next-line
requests = [_make_request(url) for url in state['urls']]
results = [request.result() for request in requests]
return {
"results": results
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"urls": ["https://www.example.com"]}, config)
```
## Resuming Workflows
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
## Starting Points for Resuming Workflows
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
+25 -25
View File
@@ -1,15 +1,22 @@
# Functional API
!!! warning "Beta"
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
## Overview
The Functional API is an alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
If modeling your application with explicit nodes and edges is not useful to you, the Functional API allows you to take advantage of LangGraph's key features for [persistence](persistence.md), [human-in-the-loop](human_in_the_loop.md) workflows, and [streaming](streaming.md) without explicitly specifying state, or control flow in terms of nodes and edges.
It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model.
The **Functional API** and the **[Graph API](./low_level.md)** can be used together in the same application, allowing you to intermix the two paradigms if needed.
The Functional API uses two key building blocks:
- **`@entrypoint`** Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts.
- **`@task`** Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously.
This provides a minimal abstraction for building workflows with state management and streaming.
!!! tip
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms.
## Example
@@ -120,22 +127,6 @@ def workflow(topic: str) -> dict:
The workflow has been completed and the review has been added to the essay.
## Functional API vs. Graph API
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create in LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Building Blocks
The **Functional API** provides two primitives for building workflows:
- **[Entrypoint](#entrypoint)**: An **entrypoint** is a decorator that designates a function as the starting point of a workflow. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](human_in_the_loop.md).
- **[Task](#task)**: Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously from within an **entrypoint**. Invoking a **task** returns a future-like object, which can be awaited to obtain the result or resolved synchronously.
## Entrypoint
The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./low_level.md#interrupt).
@@ -146,7 +137,7 @@ An **entrypoint** is defined by decorating a function with the `@entrypoint` dec
The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument.
Decorating a function with an `entrypoint` produces a Pregel instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
Decorating a function with an `entrypoint` produces a [`Pregel`][langgraph.pregel.Pregel.stream] instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**.
@@ -223,7 +214,7 @@ When declaring an `entrypoint`, you can request access to additional parameters
### Executing
Using the [`@entrypoint`](#entrypoint) yields a Pregel object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
Using the [`@entrypoint`](#entrypoint) yields a [`Pregel`][langgraph.pregel.Pregel.stream] object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
=== "Invoke"
@@ -534,6 +525,14 @@ While different runs of a workflow can produce different results, resuming a **s
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
## Functional API vs. Graph API
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Common Pitfalls
@@ -830,7 +829,8 @@ from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
# Global variable to track the number of attempts
# This variable is just used for demonstration purposes to simulate a network failure.
# It's not something you will have in your actual code.
attempts = 0
@task()
+1 -1
View File
@@ -19,7 +19,7 @@ LangGraph has a [persistence layer](https://langchain-ai.github.io/langgraph/con
### Streaming
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/stream-updates.ipynb)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/streaming.ipynb#updates)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
### Debugging and Deployment
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

+9 -9
View File
@@ -7,7 +7,7 @@ description: Conceptual Guide for LangGraph
This guide provides explanations of the key concepts behind the LangGraph framework and AI applications more broadly.
We recommend that you go through at least the [Quick Start](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
We recommend that you go through at least the [Quickstart](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
The conceptual guide does not cover step-by-step instructions or specific implementation examples — those are found in the [Tutorials](../tutorials/index.md) and [How-to guides](../how-tos/index.md). For detailed reference material, please see the [API reference](../reference/index.md).
@@ -26,9 +26,10 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
- [Durable Execution](durable_execution.md): LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
## LangGraph Platform
@@ -37,7 +38,6 @@ LangGraph Platform is a commercial solution for deploying agentic applications i
The LangGraph Platform offers a few different deployment options described in the [deployment options guide](./deployment_options.md).
!!! tip
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
@@ -46,6 +46,7 @@ The LangGraph Platform offers a few different deployment options described in th
### High Level
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
- [Platform Architecture](./platform_architecture.md): A high-level overview of the architecture of the LangGraph Platform.
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
@@ -54,7 +55,7 @@ The LangGraph Platform offers a few different deployment options described in th
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
- [LangGraph Studio](./langgraph_studio.md): LangGraph Studio is a specialized IDE that can connect to a LangGraph Server to enable visualization, interaction, and debugging of the application locally.
- [LangGraph CLI](./langgraph_cli.md): LangGraph CLI is a command-line interface that helps to interact with a local LangGraph
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
@@ -71,8 +72,7 @@ The LangGraph Platform comprises several components that work together to suppor
### Deployment Options
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
- [Cloud SaaS](./langgraph_cloud.md): Hosted as part of LangSmith.
- [Bring Your Own Cloud](./bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
+12 -6
View File
@@ -21,6 +21,12 @@ Resource Allocation:
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
## Revision
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for creating a new revision.
## Persistence
A dedicated database is automatically created for each deployment. The database serves as the [persistence layer](../concepts/persistence.md) for the deployment.
@@ -41,12 +47,6 @@ Scale down actions are delayed for 30 minutes before any action is taken. In oth
In the future, the autoscaling implementation may evolve to accommodate other metrics such as background run queue size.
## Revision
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for creating a new revision.
## Asynchronous Deployment
Infrastructure for [deployments](#deployment) and [revisions](#revision) are provisioned and deployed asynchronously. They are not deployed immediately after submission. Currently, deployment can take up to several minutes.
@@ -55,6 +55,12 @@ Infrastructure for [deployments](#deployment) and [revisions](#revision) are pro
- When a subsequent revision is created for a deployment, there is no database creation step. The deployment time for a subsequent revision is significantly faster compared to the deployment time of the initial revision.
- The deployment process for each revision contains a build step, which can take up to a few minutes.
## LangSmith Integration
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployemnt. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING_V2` and `LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set internally, automatically. Traces are created for each run and are emitted to the tracing project automatically.
When a deployment is deleted, the traces and the tracing project are not deleted.
## Automatic Deletion
Deployments are automatically deleted after 28 consecutive days of non-use (it is in an unused state). A deployment is in an unused state if there are no traces emitted to LangSmith from the deployment after 28 consecutive days. On any given day, if a deployment emits a trace to LangSmith, the counter for consecutive days of non-use is reset.
+14 -14
View File
@@ -22,10 +22,6 @@ A super-step can be considered a single iteration over the graph nodes. Nodes th
The `StateGraph` class is the main graph class to use. This is parameterized by a user defined `State` object.
### MessageGraph
The `MessageGraph` class is a special type of graph. The `State` of a `MessageGraph` is ONLY a list of messages. This class is rarely used except for chatbots, as most applications require the `State` to be more complex than a list of messages.
### Compiling your graph
To build your graph, you first define the [state](#state), you then add [nodes](#nodes) and [edges](#edges), and then you compile it. What exactly is compiling your graph and why is it needed?
@@ -217,9 +213,9 @@ builder.add_node("other_node", my_other_node)
...
```
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
Behind the scenes, functions are converted to [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)s, which add batch and async support to your function, along with native tracing and debugging.
If you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.
If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
```python
builder.add_node(my_node)
@@ -228,7 +224,7 @@ builder.add_node(my_node)
### `START` Node
The `START` Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
```python
from langgraph.graph import START
@@ -273,9 +269,9 @@ If you want to **optionally** route to 1 or more edges (or optionally terminate)
graph.add_conditional_edges("node_a", routing_function)
```
Similar to nodes, the `routing_function` accept the current `state` of the graph and return a value.
Similar to nodes, the `routing_function` accepts the current `state` of the graph and returns a value.
By default, the return value `routing_function` is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
By default, the return value `routing_function` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
@@ -314,7 +310,7 @@ graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "no
## `Send`
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common of example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
@@ -361,7 +357,7 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
### Navigating to a node in a parent graph
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
If you are using [subgraphs](#subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
@@ -376,6 +372,10 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
Setting `graph` to `Command.PARENT` will navigate to the closest parent graph.
!!! important "State updates with `Command.PARENT`"
When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](../how-tos/command.ipynb#navigating-to-a-node-in-a-parent-graph).
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
### Using inside tools
@@ -400,7 +400,7 @@ def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: R
!!! important
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node.
### Human-in-the-loop
@@ -494,7 +494,7 @@ Read more about how the `interrupt` is used for **human-in-the-loop** workflows
## Breakpoints
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt) for this purpose.
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
@@ -531,7 +531,7 @@ Let's take a look at examples for each.
### As a compiled graph
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should use write a function [invoking the subgraph](#as-a-function) instead.
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should write a function [invoking the subgraph](#as-a-function) instead.
!!! Note
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
+6 -5
View File
@@ -241,7 +241,7 @@ To address this, you can design your system _hierarchically_. For example, you c
from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.types import Command
model = ChatOpenAI()
# define team 1 (same as the single supervisor example above)
@@ -286,7 +286,7 @@ team_2_graph = team_2_builder.compile()
# define top-level supervisor
builder = StateGraph(MessagesState)
def top_level_supervisor(state: MessagesState):
def top_level_supervisor(state: MessagesState) -> Command[Literal["team_1_graph", "team_2_graph", END]]:
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# to determine which team to call next. a common pattern is to call the model
# with a structured output (e.g. force it to return an output with a "next_team" field)
@@ -297,10 +297,11 @@ def top_level_supervisor(state: MessagesState):
builder = StateGraph(MessagesState)
builder.add_node(top_level_supervisor)
builder.add_node(team_1_graph)
builder.add_node(team_2_graph)
builder.add_node("team_1_graph", team_1_graph)
builder.add_node("team_2_graph", team_2_graph)
builder.add_edge(START, "top_level_supervisor")
builder.add_edge("team_1_graph", "top_level_supervisor")
builder.add_edge("team_2_graph", "top_level_supervisor")
graph = builder.compile()
```
+1 -1
View File
@@ -433,7 +433,7 @@ See the [deployment guide](../cloud/deployment/semantic_search.md) for more deta
Under the hood, checkpointing is powered by checkpointer objects that conform to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries:
* `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([MemorySaver][langgraph.checkpoint.memory.MemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included.
* `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([InMemorySaver][langgraph.checkpoint.memory.InMemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included.
* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately.
* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.AsyncPostgresSaver]), used in LangGraph Cloud. Ideal for using in production. Needs to be installed separately.
+1 -1
View File
@@ -22,7 +22,7 @@ There are three different plans for using it.
| Real-time streaming of outputs and intermediate steps | ✅ | ✅ | ✅ |
| Assistants API (configurable templates for LangGraph apps) | ✅ | ✅ | ✅ |
| Cron scheduling | -- | ✅ | ✅ |
| LangGraph Studio for prototyping | Desktop only | Coming Soon! | Coming Soon! |
| LangGraph Studio for prototyping | | | |
| Authentication & authorization to call the LangGraph APIs | -- | Coming Soon! | Coming Soon! |
| Smart caching to reduce traffic to LLM API | -- | Coming Soon! | Coming Soon! |
| Publish/subscribe API for state | -- | Coming Soon! | Coming Soon! |
@@ -0,0 +1,23 @@
# LangGraph Platform Architecture
![](img/langgraph_platform_deployment_architecture.png)
## How we use Postgres
Postgres is the persistence layer for all user and run data in LGP. This stores both checkpoints (see more info [here](./persistence.md)) as well as the server resources (threads, runs, assistants and crons).
## How we use Redis
Redis is used in each LGP deployment as a way for server and queue workers to communicate, and to store ephemeral metadata, more details on both below. No user/run data is stored in Redis.
### Communication
All runs in LGP are executed by the pool of background workers that are part of each deployment. In order to enable some features for those runs (such as cancellation and output streaming) we need a channel for two-way communication between the server and the worker handling a particular run. We use Redis to organize that communication.
1. A Redis list is used as a mechanism to wake up a worker as soon as a new run is created. Only a sentinel value is stored in this list, no actual run info. The run information is then retrieved from Postgres by the worker.
2. A combination of a Redis string and Redis PubSub channel is used for the server to communicate a run cancellation request to the appropriate worker.
3. A Redis PubSub channel is used by the worker to broadcast streaming output from an agent while the run is being handled. Any open `/stream` request in the server will subscribe to that channel and forward any events to the response as they arrive. No events are stored in Redis at any time.
### Ephemeral metadata
Runs in an LGP deployment may be retried for specific failures (currently only for transient Postgres errors encountered during the run). In order to limit the number of retries (currently limited to 3 attempts per run) we record the attempt number in a Redis string when is picked up. This contains no run-specific info other than its ID, and expires after a short delay.
+2 -2
View File
@@ -11,7 +11,7 @@ There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](.
### Self-Hosted Lite
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed).
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed per year).
When using the Self-Hosted Lite version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key.
@@ -34,7 +34,7 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
!!! warning "Note"
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite or Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
The LangGraph Platform Deployments view is optionally available for Self-Hosted LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
+12 -123
View File
@@ -1,17 +1,23 @@
# Streaming
LangGraph is built with first class support for streaming. There are several different ways to stream back outputs from a graph run
Building a responsive app for end-users? Real-time updates are key to keeping users engaged as your app progresses.
There are three main types of data youll want to stream:
1. Workflow progress (e.g., get state updates after each graph node is executed).
2. LLM tokens as theyre generated.
3. Custom updates (e.g., "Fetched 10/100 records").
## Streaming graph outputs (`.stream` and `.astream`)
`.stream` and `.astream` are sync and async methods for streaming back outputs from a graph run.
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.
- [`"custom"`](../how-tos/streaming-content.ipynb): This streams custom data from inside your graph nodes.
- [`"values"`](../how-tos/streaming.ipynb#values): This streams the full value of the state after each step of the graph.
- [`"updates"`](../how-tos/streaming.ipynb#updates): 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.
- [`"custom"`](../how-tos/streaming.ipynb#custom): This streams custom data from inside your graph nodes.
- [`"messages"`](../how-tos/streaming-tokens.ipynb): This streams LLM tokens and metadata for the graph node where LLM is invoked.
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
- [`"debug"`](../how-tos/streaming.ipynb#debug): This streams as much information as possible throughout the execution of the graph.
You can also specify multiple streaming modes at the same time by passing them as a list. When you do this, the streamed outputs will be tuples `(stream_mode, data)`. For example:
@@ -31,123 +37,6 @@ The below visualization shows the difference between the `values` and `updates`
![values vs updates](../static/values_vs_updates.png)
## Streaming LLM tokens and events (`.astream_events`)
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/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/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-4o-mini")
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="v1"):
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-4o-mini',
'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).
## LangGraph Platform
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. LangGraph Platform supports five streaming modes:
@@ -155,8 +44,8 @@ Streaming is critical for making LLM applications feel responsive to end users.
- `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](../cloud/how-tos/stream_values.md) for streaming values.
- `messages-tuple`: Stream LLM tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. See the [how-to guide](../cloud/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](../cloud/how-tos/stream_updates.md) for streaming updates.
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/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](../cloud/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](../cloud/how-tos/stream_events.md) for streaming events. This mode is only useful for users migrating large LCEL applications to LangGraph. Generally, this mode is not necessary for most applications.
You can also specify multiple streaming modes at the same time. See the [how-to guide](../cloud/how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
+3 -11
View File
@@ -58,7 +58,7 @@
"\n",
"This guide shows how you can:\n",
"\n",
"- implement handoffs using `Command`: agent node makes some decision (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
"- implement handoffs using `Command`: agent node makes a decision on who to hand off to (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
"- implement handoffs using tools: a tool-calling agent has access to tools that can return a handoff via `Command`. The tool-executing node in the agent recognizes `Command` objects returned by the tools and routes accordingly. Handoff tool a general-purpose primitive that is useful in any multi-agent systems that contain tool-calling agents."
]
},
@@ -83,18 +83,10 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "b4864843-00a1-4c88-9a7c-c34e6c31c548",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
+4 -2
View File
@@ -13,12 +13,14 @@
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
???+ note "Support by deployment type"
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
## 1. Implement authentication
Create `auth.py` file, with a basic JWT authentication handler:
```python
from langgraph_sdk import Auth
@@ -0,0 +1,387 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "100c0c81-6a9f-4ba1-b1a8-42aae82b7172",
"metadata": {},
"source": [
"# How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks\n",
"\n",
"LangGraph is a framework for building agentic and multi-agent applications. LangGraph can be easily integrated with other agent frameworks. \n",
"\n",
"The primary reasons you might want to integrate LangGraph with other agent frameworks:\n",
"\n",
"- create [multi-agent systems](../../concepts/multi_agent) where individual agents are built with different frameworks\n",
"- leverage LangGraph to add features like [persistence](../../concepts/persistence), [streaming](../../concepts/streaming), [short and long-term memory](../../concepts/memory) and more\n",
"\n",
"The simplest way to integrate agents from other frameworks is by calling those agents inside a LangGraph [node](../../concepts/low_level/#nodes):\n",
"\n",
"```python\n",
"import autogen\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"autogen_agent = autogen.AssistantAgent(name=\"assistant\", ...)\n",
"user_proxy = autogen.UserProxyAgent(name=\"user_proxy\", ...)\n",
"\n",
"@task\n",
"def call_autogen_agent(messages):\n",
" response = user_proxy.initiate_chat(\n",
" autogen_agent,\n",
" message=messages[-1],\n",
" ...\n",
" )\n",
" ...\n",
"\n",
"\n",
"@entrypoint()\n",
"def workflow(messages):\n",
" response = call_autogen_agent(messages).result()\n",
" return response\n",
"\n",
"\n",
"workflow.invoke(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
" }\n",
" ]\n",
")\n",
"```\n",
"\n",
"In this guide we show how to build a LangGraph chatbot that integrates with AutoGen, but you can follow the same approach with other frameworks."
]
},
{
"cell_type": "markdown",
"id": "b189ceb2-132b-4c7b-81b4-c7b8b062f833",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "62417d3a-94f9-4a52-9962-12639d714966",
"metadata": {},
"outputs": [],
"source": [
"%pip install autogen langgraph"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "d46da41d-0a71-4654-aec8-9e6ad8765236",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
"metadata": {},
"source": [
"## Define AutoGen agent\n",
"\n",
"Here we define our AutoGen agent. Adapted from official tutorial [here](https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb)."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "524de117-ff09-4b26-bfe8-a9f85a46ffd5",
"metadata": {},
"outputs": [],
"source": [
"import autogen\n",
"import os\n",
"\n",
"config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
"\n",
"llm_config = {\n",
" \"timeout\": 600,\n",
" \"cache_seed\": 42,\n",
" \"config_list\": config_list,\n",
" \"temperature\": 0,\n",
"}\n",
"\n",
"autogen_agent = autogen.AssistantAgent(\n",
" name=\"assistant\",\n",
" llm_config=llm_config,\n",
")\n",
"\n",
"user_proxy = autogen.UserProxyAgent(\n",
" name=\"user_proxy\",\n",
" human_input_mode=\"NEVER\",\n",
" max_consecutive_auto_reply=10,\n",
" is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
" code_execution_config={\n",
" \"work_dir\": \"web\",\n",
" \"use_docker\": False,\n",
" }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
" llm_config=llm_config,\n",
" system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "8aa858e2-4acb-4f75-be20-b9ccbbcb5073",
"metadata": {},
"source": [
"---"
]
},
{
"cell_type": "markdown",
"id": "dcc478f5-4a35-43f8-bf59-9cb71289cd00",
"metadata": {},
"source": [
"## Create the workflow\n",
"\n",
"We will now create a LangGraph chatbot graph that calls AutoGen agent."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@task\n",
"def call_autogen_agent(messages: list[BaseMessage]):\n",
" # convert to openai-style messages\n",
" messages = convert_to_openai_messages(messages)\n",
" response = user_proxy.initiate_chat(\n",
" autogen_agent,\n",
" message=messages[-1],\n",
" # pass previous message history as context\n",
" carryover=messages[:-1],\n",
" )\n",
" # get the final response from the agent\n",
" content = response.chat_history[-1][\"content\"]\n",
" return {\"role\": \"assistant\", \"content\": content}\n",
"\n",
"\n",
"# add short-term memory for storing conversation history\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def workflow(messages: list[BaseMessage], previous: list[BaseMessage]):\n",
" messages = add_messages(previous or [], messages)\n",
" response = call_autogen_agent(messages).result()\n",
" return entrypoint.final(value=response, save=add_messages(messages, response))"
]
},
{
"cell_type": "markdown",
"id": "23d629c3-1d6b-40af-adf6-915e15657566",
"metadata": {},
"source": [
"## Run the graph\n",
"\n",
"We can now run the graph."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "a279b667-0f5d-4008-8d43-c806a3f379c4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Find numbers between 10 and 30 in fibonacci sequence\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
"\n",
"1. Generate Fibonacci numbers starting from 0.\n",
"2. Continue generating until the numbers exceed 30.\n",
"3. Collect and print the numbers that are between 10 and 30.\n",
"\n",
"Let's implement this in Python:\n",
"\n",
"```python\n",
"# filename: fibonacci_range.py\n",
"\n",
"def fibonacci_sequence():\n",
" a, b = 0, 1\n",
" while a <= 30:\n",
" if 10 <= a <= 30:\n",
" print(a)\n",
" a, b = b, a + b\n",
"\n",
"fibonacci_sequence()\n",
"```\n",
"\n",
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001B[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"exitcode: 0 (execution succeeded)\n",
"Code output: \n",
"13\n",
"21\n",
"\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
"\n",
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
"\n",
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
"\n",
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
"\n",
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n",
"{'workflow': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n"
]
}
],
"source": [
"# pass the thread ID to persist agent outputs for future interactions\n",
"# highlight-next-line\n",
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"\n",
"for chunk in workflow.stream(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
" }\n",
" ],\n",
" # highlight-next-line\n",
" config,\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "c6cd57b4-d4ee-49f6-be12-318613849669",
"metadata": {},
"source": [
"Since we're leveraging LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) features we can now continue the conversation using the same thread ID -- LangGraph will automatically pass previous history to the AutoGen agent:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "e68811a7-962e-4fe3-9f45-9b99ebbe04e7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Multiply the last number by 3\n",
"Context: \n",
"Find numbers between 10 and 30 in fibonacci sequence\n",
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
"\n",
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
"\n",
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
"\n",
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
"\n",
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
"\n",
"21 * 3 = 63\n",
"\n",
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n",
"{'workflow': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n"
]
}
],
"source": [
"for chunk in workflow.stream(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"Multiply the last number by 3\",\n",
" }\n",
" ],\n",
" # highlight-next-line\n",
" config,\n",
"):\n",
" print(chunk)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36 -9
View File
@@ -16,10 +16,10 @@
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [State](../../concepts/low_level/#state)\n",
" - [Nodes](../../concepts/low_level/#nodes)\n",
" - [Edges](../../concepts/low_level/#edges)\n",
" - [Command](../../concepts/low_level/#command)\n",
" - [State](../../concepts/low_level#state)\n",
" - [Nodes](../../concepts/low_level#nodes)\n",
" - [Edges](../../concepts/low_level#edges)\n",
" - [Command](../../concepts/low_level#command)\n",
"\n",
"It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a `Command` object from node functions:\n",
"\n",
@@ -44,6 +44,10 @@
" )\n",
"```\n",
"\n",
"!!! important \"State updates with `Command.PARENT`\"\n",
"\n",
" When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](../../concepts/low_level#schema), you **must** define a [reducer](../../concepts/low_level#reducers) for the key you're updating in the parent graph state. See this [example](#navigating-to-a-node-in-a-parent-graph) below.\n",
"\n",
"This guide shows how you can do use `Command` to add dynamic control flow in your LangGraph app."
]
},
@@ -224,13 +228,13 @@
"output_type": "stream",
"text": [
"Called A\n",
"Called B\n"
"Called C\n"
]
},
{
"data": {
"text/plain": [
"{'foo': 'ab'}"
"{'foo': 'bc'}"
]
},
"execution_count": 5,
@@ -258,6 +262,16 @@
"Now let's demonstrate how you can navigate from inside a subgraph to a different node in a parent graph. We'll do so by changing `node_a` in the above example into a single-node graph that we'll add as a subgraph to our parent graph."
]
},
{
"cell_type": "markdown",
"id": "6be0aeb9-e138-4adc-a1df-5d743a8eb348",
"metadata": {},
"source": [
"!!! important \"State updates with `Command.PARENT`\"\n",
"\n",
" When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](../../concepts/low_level#schema), you **must** define a [reducer](../../concepts/low_level#reducers) for the key you're updating in the parent graph state."
]
},
{
"cell_type": "code",
"execution_count": 6,
@@ -265,7 +279,14 @@
"metadata": {},
"outputs": [],
"source": [
"# Define the nodes\n",
"import operator\n",
"from typing_extensions import Annotated\n",
"\n",
"\n",
"class State(TypedDict):\n",
" # NOTE: we define a reducer here\n",
" # highlight-next-line\n",
" foo: Annotated[str, operator.add]\n",
"\n",
"\n",
"def node_a(state: State):\n",
@@ -283,6 +304,7 @@
" goto=goto,\n",
" # this tells LangGraph to navigate to node_b or node_c in the parent graph\n",
" # NOTE: this will navigate to the closest parent graph relative to the subgraph\n",
" # highlight-next-line\n",
" graph=Command.PARENT,\n",
" )\n",
"\n",
@@ -292,12 +314,17 @@
"\n",
"def node_b(state: State):\n",
" print(\"Called B\")\n",
" return {\"foo\": state[\"foo\"] + \"b\"}\n",
" # NOTE: since we've defined a reducer, we don't need to manually append\n",
" # new characters to existing 'foo' value. instead, reducer will append these\n",
" # automatically (via operator.add)\n",
" # highlight-next-line\n",
" return {\"foo\": \"b\"}\n",
"\n",
"\n",
"def node_c(state: State):\n",
" print(\"Called C\")\n",
" return {\"foo\": state[\"foo\"] + \"c\"}"
" # highlight-next-line\n",
" return {\"foo\": \"c\"}"
]
},
{
+34 -26
View File
@@ -9,16 +9,24 @@ Here youll find answers to “How do I...?” types of questions. These guide
## LangGraph
### Controllability
LangGraph offers a high level of control over the execution of your graph.
These how-to guides show how to achieve that controllability.
### Graph API Basics
- [How to update graph state from nodes](state-reducers.ipynb)
- [How to create a sequence of steps](sequence.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to create and control loops with recursion limits](recursion-limit.ipynb)
- [How to visualize your graph](visualization.ipynb)
### Fine-grained Control
These guides demonstrate LangGraph features that grant fine-grained control over the
execution of your graph.
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to control graph recursion limit](recursion-limit.ipynb)
- [How to combine control flow and state updates with Command](command.ipynb)
- [How to update state and jump to nodes in graphs and subgraphs](command.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to add node retries](node-retries.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
### Persistence
@@ -31,8 +39,7 @@ These how-to guides show how to achieve that controllability.
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
See the below guides for how-to add persistence to your workflow using the (beta)
[Functional API](../concepts/functional_api.md):
See the below guides for how-to add persistence to your workflow using the [Functional API](../concepts/functional_api.md):
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
@@ -65,7 +72,7 @@ Other methods:
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
See the below guides for how-to implement human-in-the-loop workflows with the
[Functional API](../concepts/functional_api.md):
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
@@ -81,15 +88,10 @@ See the below guides for how-to implement human-in-the-loop workflows with the (
[Streaming](../concepts/streaming.md) is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [How to stream full state of your graph](stream-values.ipynb)
- [How to stream state updates of your graph](stream-updates.ipynb)
- [How to stream](streaming.ipynb)
- [How to stream LLM tokens](streaming-tokens.ipynb)
- [How to stream LLM tokens without LangChain models](streaming-tokens-without-langchain.ipynb)
- [How to stream custom data](streaming-content.ipynb)
- [How to configure multiple streaming modes at the same time](stream-multiple.ipynb)
- [How to stream events from within a tool](streaming-events-from-within-tools.ipynb)
- [How to stream events from within a tool without LangChain models](streaming-events-from-within-tools-without-langchain.ipynb)
- [How to stream events from the final node](streaming-from-final-node.ipynb)
- [How to stream LLM tokens from specific nodes](streaming-specific-nodes.ipynb)
- [How to stream data from within a tool](streaming-events-from-within-tools.ipynb)
- [How to stream from subgraphs](streaming-subgraphs.ipynb)
- [How to disable streaming for models that don't support it](disable-streaming.ipynb)
@@ -127,8 +129,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
See the below guides for how-to implement multi-agent workflows with the (beta)
[Functional API](../concepts/functional_api.md):
See the below guides for how to implement multi-agent workflows with the [Functional API](../concepts/functional_api.md):
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
@@ -142,14 +143,14 @@ See the below guides for how-to implement multi-agent workflows with the (beta)
### Other
- [How to run graph asynchronously](async.ipynb)
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to add node retries](node-retries.ipynb)
- [How to force tool-calling agent to structure output](react-agent-structured-output.ipynb)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
### Prebuilt ReAct Agent
The LangGraph [prebuilt ReAct agent](../reference/prebuilt.md#langgraph.prebuilt.chat_agent_executor.create_react_agent) is pre-built implementation of a [tool calling agent](../concepts/agentic_concepts.md#tool-calling-agent).
@@ -170,8 +171,7 @@ overview of its underlying implementation to help you customize for your own nee
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
See the below guide for how-to build ReAct agents with the (beta)
[Functional API](../concepts/functional_api.md):
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
@@ -200,6 +200,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
### Deployment
@@ -289,6 +290,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
- [How to engineer prompts in LangGraph Studio](../cloud/how-tos/iterate_graph_studio.md)
## Troubleshooting
@@ -299,3 +301,9 @@ These are the guides for resolving common errors you may find while building wit
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.md)
- [INVALID_CHAT_HISTORY](../troubleshooting/errors/INVALID_CHAT_HISTORY.md)
### LangGraph Platform Troubleshooting
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
- [INVALID_LICENSE](../troubleshooting/errors/INVALID_LICENSE.md)
+1 -1
View File
@@ -207,7 +207,7 @@
"\n",
"\n",
"# Here we define the logic to map out over the generated subjects\n",
"# We will use this an edge in the graph\n",
"# We will use this as an edge in the graph\n",
"def continue_to_jokes(state: OverallState):\n",
" # We will return a list of `Send` objects\n",
" # Each `Send` object consists of the name of a node in the graph\n",
@@ -88,8 +88,8 @@
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
"# %%capture --no-stderr\n",
"# %pip install -U langgraph langchain-anthropic"
]
},
{
@@ -99,7 +99,7 @@
"metadata": {},
"outputs": [
{
"name": "stdout",
"name": "stdin",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
@@ -212,11 +212,13 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 6,
"id": "aa4bdbff-9461-46cc-aee9-8a22d3c3d9ec",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"from langchain_core.messages import AIMessage\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.prebuilt import create_react_agent\n",
@@ -273,11 +275,14 @@
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"def string_to_uuid(input_string):\n",
" return str(uuid.uuid5(uuid.NAMESPACE_URL, input_string))\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def multi_turn_graph(messages, previous):\n",
" previous = previous or []\n",
" messages = add_messages(previous, messages)\n",
"\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
@@ -292,7 +297,17 @@
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
" if not ai_msg.tool_calls:\n",
" user_input = interrupt(value=\"Ready for user input.\")\n",
" messages = add_messages(messages, [{\"role\": \"user\", \"content\": user_input}])\n",
" # Add user input as a human message\n",
" # NOTE: we generate unique ID for the human message based on its content\n",
" # it's important, since on subsequent invocations previous user input (interrupt) values\n",
" # will be looked up again and we will attempt to add them again here\n",
" # `add_messages` deduplicates messages based on the ID, ensuring correct message history\n",
" human_message = {\n",
" \"role\": \"user\",\n",
" \"content\": user_input,\n",
" \"id\": string_to_uuid(user_input),\n",
" }\n",
" messages = add_messages(messages, [human_message])\n",
" continue\n",
"\n",
" tool_call = ai_msg.tool_calls[-1]\n",
@@ -318,8 +333,8 @@
},
{
"cell_type": "code",
"execution_count": 5,
"id": "161e0cf1-d13a-4026-8f89-bdab67d1ad4d",
"execution_count": 7,
"id": "2b6fde57-86e3-440e-a7bf-f1e9b5ed9ff2",
"metadata": {},
"outputs": [
{
@@ -329,71 +344,69 @@
"\n",
"--- Conversation Turn 1 ---\n",
"\n",
"User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}\n",
"User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean', 'id': 'f48d82a7-7efa-43f5-ad4c-541758c95f61'}\n",
"\n",
"call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known for its perfect warm weather year-round, with consistent temperatures around 82°F (28°C) and very little rainfall. The island offers:\n",
"call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Known as \"One Happy Island,\" Aruba offers:\n",
"- Year-round warm weather with consistent temperatures around 82°F (28°C)\n",
"- Beautiful white sand beaches like Eagle Beach and Palm Beach\n",
"- Crystal clear waters perfect for swimming and snorkeling\n",
"- Minimal rainfall and location outside the hurricane belt\n",
"- Rich culture blending Dutch and Caribbean influences\n",
"- Various activities from water sports to desert-like landscape exploration\n",
"- Excellent dining and shopping options\n",
"\n",
"1. Beautiful white-sand beaches like Eagle Beach and Palm Beach\n",
"2. Crystal clear waters perfect for swimming and snorkeling\n",
"3. Constant cooling trade winds that make the warm weather comfortable\n",
"4. A mix of luxury resorts and boutique hotels\n",
"5. Diverse activities from water sports to desert-like terrain exploration\n",
"6. Great dining and nightlife options\n",
"7. Safe and tourist-friendly environment\n",
"\n",
"Would you like me to connect you with our hotel advisor to help you find the perfect place to stay in Aruba?\n",
"Would you like me to help you find suitable accommodations in Aruba? I can transfer you to our hotel advisor who can recommend specific hotels based on your preferences.\n",
"\n",
"--- Conversation Turn 2 ---\n",
"\n",
"User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')\n",
"\n",
"call_hotel_advisor: Based on the recommendations, I can highlight two excellent options in different areas:\n",
"call_hotel_advisor: I can recommend two excellent options in different areas:\n",
"\n",
"1. The Ritz-Carlton, Aruba - Located in Palm Beach\n",
"- Part of the high-rise hotel district\n",
"- Luxury beachfront resort with full-service spa\n",
"- Multiple restaurants and a casino\n",
"- Perfect for those who want to be in the heart of the action\n",
"- Close to shopping, dining, and nightlife\n",
"- Luxury beachfront resort\n",
"- Located in the vibrant Palm Beach area, known for its lively atmosphere\n",
"- Close to restaurants, shopping, and nightlife\n",
"- Perfect for those who want a more active vacation with plenty of amenities nearby\n",
"\n",
"2. Bucuti & Tara Beach Resort - Located in Eagle Beach\n",
"- Adults-only boutique resort\n",
"- Located on Eagle Beach, voted one of the best beaches in the world\n",
"- More serene and romantic atmosphere\n",
"- Perfect for couples and those seeking a quieter vacation\n",
"- Known for its excellent service and sustainability practices\n",
"- Situated on the quieter Eagle Beach\n",
"- Known for its romantic atmosphere and excellent service\n",
"- Ideal for couples seeking a more peaceful, intimate setting\n",
"\n",
"Would you like more specific information about either of these properties or would you like to explore other options in either area?\n",
"Would you like more specific information about either of these properties or their locations?\n",
"\n",
"--- Conversation Turn 3 ---\n",
"\n",
"User: Command(resume='i like the first one. could you recommend something to do near the hotel?')\n",
"\n",
"call_travel_advisor: Near the Ritz-Carlton in Palm Beach, you can enjoy several fantastic activities:\n",
"call_travel_advisor: Near The Ritz-Carlton in Palm Beach, here are some popular activities you can enjoy:\n",
"\n",
"1. Paseo Herencia Mall - A beautiful outdoor shopping and entertainment center just a short walk away\n",
"2. High-Rise Beach Strip - Perfect for beach walks and water sports\n",
"3. Bubali Bird Sanctuary - A nature preserve where you can spot local wildlife\n",
"4. The Butterfly Farm - A unique attraction featuring hundreds of exotic butterflies\n",
"5. Palm Beach Plaza Mall - Great for shopping and dining\n",
"6. Various water sports operators offering:\n",
" - Jet skiing\n",
" - Parasailing\n",
" - Snorkeling trips\n",
" - Sunset sailing cruises\n",
"1. Palm Beach Strip - Take a walk along this bustling strip filled with restaurants, shops, and bars\n",
"2. Visit the Bubali Bird Sanctuary - Just a short distance away\n",
"3. Try your luck at the Stellaris Casino - Located right in The Ritz-Carlton\n",
"4. Water Sports at Palm Beach - Right in front of the hotel you can:\n",
" - Go parasailing\n",
" - Try jet skiing\n",
" - Take a sunset sailing cruise\n",
"5. Visit the Palm Beach Plaza Mall - High-end shopping just a short walk away\n",
"6. Enjoy dinner at Madame Janette's - One of Aruba's most famous restaurants nearby\n",
"\n",
"Additionally, the hotel concierge can arrange most activities directly for you. Would you like more specific information about any of these activities?\n"
"Would you like more specific information about any of these activities or other suggestions in the area?\n"
]
}
],
"source": [
"import uuid\n",
"\n",
"thread_config = {\"configurable\": {\"thread_id\": uuid.uuid4()}}\n",
"\n",
"inputs = [\n",
" # 1st round of conversation,\n",
" {\"role\": \"user\", \"content\": \"i wanna go somewhere warm in the caribbean\"},\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"i wanna go somewhere warm in the caribbean\",\n",
" \"id\": str(uuid.uuid4()),\n",
" },\n",
" # Since we're using `interrupt`, we'll need to resume using the Command primitive.\n",
" # 2nd round of conversation,\n",
" Command(\n",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-186
View File
@@ -1,186 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3631f2b9-aa79-472e-a9d6-9125a90ee704",
"metadata": {},
"source": [
"# How to stream state updates of your graph"
]
},
{
"cell_type": "markdown",
"id": "858c7499-0c92-40a9-bd95-e5a5a5817e92",
"metadata": {},
"source": [
"LangGraph supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `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.\n",
"\n",
"This guide covers `stream_mode=\"updates\"`."
]
},
{
"cell_type": "markdown",
"id": "7c2f84f1-0751-4779-97d4-5cbb286093b7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required package and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6b4285e4-7434-4971-bde0-aabceef8ee7e",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai langchain-community"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f7f9f24a-e3d0-422b-8924-47950b2facd6",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "cc6c48fe",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "2e7777f9",
"metadata": {},
"source": [
"## Define the graph\n",
"\n",
"We'll be using a simple ReAct agent for this guide."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "85cf2e23-29f2-40cc-b302-5377b3b49da9",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
"graph = create_react_agent(model, tools)"
]
},
{
"cell_type": "markdown",
"id": "956db549-5207-4be1-a823-78311738e3f8",
"metadata": {},
"source": [
"## Stream updates"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e9e9ffb0-2cd5-466f-b70b-b6ed51b852d1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving update from node: 'agent'\n",
"{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_kc6cvcEkTAUGRlSHrP4PK9fn', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-cd68b3a0-86c3-4afa-9649-1b962a0dd062-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_kc6cvcEkTAUGRlSHrP4PK9fn'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})]}\n",
"\n",
"\n",
"\n",
"Receiving update from node: 'tools'\n",
"{'messages': [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', tool_call_id='call_kc6cvcEkTAUGRlSHrP4PK9fn')]}\n",
"\n",
"\n",
"\n",
"Receiving update from node: 'agent'\n",
"{'messages': [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'stop', 'logprobs': None}, id='run-009d83c4-b874-4acc-9494-20aba43132b9-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"updates\"):\n",
" for node, values in chunk.items():\n",
" print(f\"Receiving update from node: '{node}'\")\n",
" print(values)\n",
" print(\"\\n\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-248
View File
@@ -1,248 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3631f2b9-aa79-472e-a9d6-9125a90ee704",
"metadata": {},
"source": [
"# How to stream full state of your graph"
]
},
{
"cell_type": "markdown",
"id": "858c7499-0c92-40a9-bd95-e5a5a5817e92",
"metadata": {},
"source": [
"LangGraph supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `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.\n",
"\n",
"This guide covers `stream_mode=\"values\"`."
]
},
{
"cell_type": "markdown",
"id": "7c2f84f1-0751-4779-97d4-5cbb286093b7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6b4285e4-7434-4971-bde0-aabceef8ee7e",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai langchain-community"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f7f9f24a-e3d0-422b-8924-47950b2facd6",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "eaaab1fc",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "7939a3c5",
"metadata": {},
"source": [
"## Define the graph\n",
"\n",
"We'll be using a simple ReAct agent for this guide."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "ef5a3ec6-0cd0-4541-ab1b-d63ede22720e",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
"graph = create_react_agent(model, tools)"
]
},
{
"cell_type": "markdown",
"id": "002a715b-e0be-4e89-8d42-f0098882586b",
"metadata": {},
"source": [
"## Stream values"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e9e9ffb0-2cd5-466f-b70b-b6ed51b852d1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"what's the weather in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_61VvIzqVGtyxcXi0z6knZkjZ)\n",
" Call ID: call_61VvIzqVGtyxcXi0z6knZkjZ\n",
" Args:\n",
" city: sf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny.\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "d73de237-bf45-4fa7-93ef-6dae7eacffc0",
"metadata": {},
"source": [
"If we want to just get the final result, we can use the same method and just keep track of the last value we received"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c122bf15-a489-47bf-b482-a744a54e2cc4",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"values\"):\n",
" final_result = chunk"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "316022e5-4c65-48e4-9878-8d94a2425ed4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'messages': [HumanMessage(content=\"what's the weather in sf\", id='54b39b6f-054b-4306-980b-86905e48a6bc'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_avoKnK8reERzTUSxrN9cgFxY', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_5e6c71d4a8', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-f2f43c89-2c96-45f4-975c-2d0f22d0d2d1-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_avoKnK8reERzTUSxrN9cgFxY'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='fc18a798-c7b2-4f73-84fa-8ffdffb6ddcb', tool_call_id='call_avoKnK8reERzTUSxrN9cgFxY'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny. Enjoy the sunshine!', response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 84, 'total_tokens': 98}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_5e6c71d4a8', 'finish_reason': 'stop', 'logprobs': None}, id='run-21418147-da8e-4738-a076-239377397c40-0', usage_metadata={'input_tokens': 84, 'output_tokens': 14, 'total_tokens': 98})]}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"final_result"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "0f64ebbe-535c-4b35-a95f-0a7490cfed90",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny. Enjoy the sunshine!\n"
]
}
],
"source": [
"final_result[\"messages\"][-1].pretty_print()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-346
View File
@@ -1,346 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "15c4bd28",
"metadata": {},
"source": [
"# How to stream custom data\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
" <p>\n",
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li> \n",
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/streaming/\">\n",
" Streaming\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#astream_events\">\n",
" astream_events API\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
" </ul>\n",
" </p>\n",
"</div>\n",
"\n",
"The most common use case for streaming from inside a node is to stream LLM tokens, but you may also want to stream custom data.\n",
"\n",
"For example, if you have a long-running tool call, you can dispatch custom events between the steps and use these custom events to monitor progress. You could also surface these custom events to an end user of your application to show them how the current task is progressing.\n",
"\n",
"You can do so in two ways:\n",
"* using graph's `.stream` / `.astream` methods with `stream_mode=\"custom\"`\n",
"* emitting custom events using [adispatch_custom_events](https://python.langchain.com/docs/how_to/callbacks_custom_events/).\n",
"\n",
"Below we'll see how to use both APIs.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install our required packages"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "e1a20f31",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph"
]
},
{
"cell_type": "markdown",
"id": "12297071",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "29814253-ca9b-4844-a8a5-d6b19fbdbdba",
"metadata": {},
"source": [
"## Stream custom data using `.stream / .astream`"
]
},
{
"cell_type": "markdown",
"id": "b729644a-b65f-4e69-ad45-f2e88ffb4e9d",
"metadata": {},
"source": [
"### Define the graph"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9731c40f-5ce7-460d-b2ad-33185529c99d",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langgraph.graph import START, StateGraph, MessagesState, END\n",
"from langgraph.types import StreamWriter\n",
"\n",
"\n",
"async def my_node(\n",
" state: MessagesState,\n",
" writer: StreamWriter, # <-- provide StreamWriter to write chunks to be streamed\n",
"):\n",
" chunks = [\n",
" \"Four\",\n",
" \"score\",\n",
" \"and\",\n",
" \"seven\",\n",
" \"years\",\n",
" \"ago\",\n",
" \"our\",\n",
" \"fathers\",\n",
" \"...\",\n",
" ]\n",
" for chunk in chunks:\n",
" # write the chunk to be streamed using stream_mode=custom\n",
" writer(chunk)\n",
"\n",
" return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"workflow.add_node(\"model\", my_node)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
},
{
"cell_type": "markdown",
"id": "ecd69eed-9624-4640-b0af-c9f82b190900",
"metadata": {},
"source": [
"### Stream content"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "00a91b15-82c7-443c-acb6-a7406df15cee",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Four\n",
"score\n",
"and\n",
"seven\n",
"years\n",
"ago\n",
"our\n",
"fathers\n",
"...\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
"async for chunk in app.astream({\"messages\": inputs}, stream_mode=\"custom\"):\n",
" print(chunk, flush=True)"
]
},
{
"cell_type": "markdown",
"id": "c7b9f1f0-c170-40dc-9c22-289483dfbc99",
"metadata": {},
"source": [
"You will likely need to use [multiple streaming modes](https://langchain-ai.github.io/langgraph/how-tos/stream-multiple/) as you will\n",
"want access to both the custom data and the state updates."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "f8ed22d4-6ce6-4b04-a68b-2ea516e3ab15",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"('custom', 'Four')\n",
"('custom', 'score')\n",
"('custom', 'and')\n",
"('custom', 'seven')\n",
"('custom', 'years')\n",
"('custom', 'ago')\n",
"('custom', 'our')\n",
"('custom', 'fathers')\n",
"('custom', '...')\n",
"('updates', {'model': {'messages': [AIMessage(content='Four score and seven years ago our fathers ...', additional_kwargs={}, response_metadata={})]}})\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
"async for chunk in app.astream({\"messages\": inputs}, stream_mode=[\"custom\", \"updates\"]):\n",
" print(chunk, flush=True)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "ca976d6a-7c64-4603-8bb4-dee95428c33d",
"metadata": {},
"source": [
"## Stream custom data using `.astream_events`\n",
"\n",
"If you are already using graph's `.astream_events` method in your workflow, you can also stream custom data by emitting custom events using `adispatch_custom_event`\n",
"\n",
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">ASYNC IN PYTHON<=3.10</p>\n",
" <p>\n",
"\n",
"LangChain cannot automatically propagate configuration, including callbacks necessary for `astream_events()`, to child runnables if you are running async code in python<=3.10. This is a common reason why you may fail to see events being emitted from custom runnables or tools.\n",
"\n",
"If you are running python<=3.10, you will need to manually propagate the `RunnableConfig` object to the child runnable in async environments. For an example of how to manually propagate the config, see the implementation of the node below with `adispatch_custom_event`.\n",
"\n",
"If you are running python>=3.11, the `RunnableConfig` will automatically propagate to child runnables in async environment. However, it is still a good idea to propagate the `RunnableConfig` manually if your code may run in other Python versions.\n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "b390a9fe-2d5f-4e82-a1ea-c7c0186b8559",
"metadata": {},
"source": [
"### Define the graph"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "486a01a0",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.runnables import RunnableConfig, RunnableLambda\n",
"from langchain_core.callbacks.manager import adispatch_custom_event\n",
"\n",
"\n",
"async def my_node(state: MessagesState, config: RunnableConfig):\n",
" chunks = [\n",
" \"Four\",\n",
" \"score\",\n",
" \"and\",\n",
" \"seven\",\n",
" \"years\",\n",
" \"ago\",\n",
" \"our\",\n",
" \"fathers\",\n",
" \"...\",\n",
" ]\n",
" for chunk in chunks:\n",
" await adispatch_custom_event(\n",
" \"my_custom_event\",\n",
" {\"chunk\": chunk},\n",
" config=config, # <-- propagate config\n",
" )\n",
"\n",
" return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"workflow.add_node(\"model\", my_node)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
},
{
"cell_type": "markdown",
"id": "7dcded03-6776-405e-afae-005a3212d3e4",
"metadata": {},
"source": [
"### Stream content"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "ce773a40",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Four|score|and|seven|years|ago|our|fathers|...|"
]
}
],
"source": [
"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=\"v2\"):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_custom_event\" and event[\"name\"] == \"my_custom_event\":\n",
" data = event[\"data\"]\n",
" if data:\n",
" print(data[\"chunk\"], end=\"|\", flush=True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,372 +0,0 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "18e6e213-b398-4a7e-b342-ba225e97b424",
"metadata": {},
"source": [
"# How to stream events from within a tool (without LangChain LLMs / tools)\n",
"\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
" <p>\n",
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li> \n",
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/streaming/\">\n",
" Streaming\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#astream_events\">\n",
" astream_events API\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
" </ul>\n",
" </p>\n",
"</div>\n",
"\n",
"In this guide, we will demonstrate how to stream tokens from tools used by a custom ReAct agent, without relying on LangChains chat models or tool-calling functionalities. \n",
"\n",
"We will use the OpenAI client library directly for the chat model interaction. The tool execution will be implemented from scratch.\n",
"\n",
"This showcases how LangGraph can be utilized independently of built-in LangChain components like chat models or tools.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph openai"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "0cf6b41d-7fcb-40b6-9a72-229cdd00a094",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "d8df7b58",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "7d766c7d-34ea-455b-8bcb-f2f12d100e1d",
"metadata": {},
"source": [
"## Define the graph\n",
"\n",
"### Define a node that will call OpenAI API"
]
},
{
"cell_type": "code",
"execution_count": 9,
"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]}"
]
},
{
"cell_type": "markdown",
"id": "3a3877e8-8ace-40d5-ad04-cbf21c6f3250",
"metadata": {},
"source": [
"### Define our tools and a tool-calling node"
]
},
{
"cell_type": "code",
"execution_count": 10,
"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]}"
]
},
{
"cell_type": "markdown",
"id": "6685898c-9a1c-4803-a492-bd70574ebe38",
"metadata": {},
"source": [
"### Define our graph"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Literal\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph, END, START\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.add_edge(START, \"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()"
]
},
{
"cell_type": "markdown",
"id": "d046e2ef-f208-4831-ab31-203b2e75a49a",
"metadata": {},
"source": [
"## Stream tokens from within the tool\n",
"\n",
"Here, we'll use the `astream_events` API to stream back individual events. Please see [astream_events](https://python.langchain.com/docs/concepts/#astream_events) for more details."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "45c96a79-4147-42e3-89fd-d942b2b49f6c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Tool token socks\n",
"Tool token shoes\n",
"Tool token dust bunnies\n"
]
}
],
"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": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -3,50 +3,82 @@
{
"attachments": {},
"cell_type": "markdown",
"id": "04b012ac-e0b5-483e-a645-d13d0e215aad",
"id": "695d935e-b4fe-45a6-a061-a66d32cb832b",
"metadata": {},
"source": [
"# How to stream data from within a tool\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
" <p>\n",
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li> \n",
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/streaming/\">\n",
" Streaming\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig\">\n",
" RunnableConfig\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#runnable-interface\">\n",
" RunnableInterface\n",
" </a>\n",
" </li>\n",
" </ul>\n",
" </p>\n",
"</div>\n",
"!!! info \"Prerequisites\"\n",
"\n",
"If your graph involves tools that invoke LLMs (or any other LangChain `Runnable` objects like other graphs, `LCEL` chains, or retrievers), you might want to surface partial results during the execution of the tool, especially if the tool takes a longer time to run.\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Streaming](../../concepts/streaming/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
" - [Tools](https://python.langchain.com/docs/concepts/tools/)\n",
"\n",
"A common scenario is streaming LLM tokens generated by a tool calling an LLM, though this applies to any use of Runnable objects. \n",
"If your graph calls tools that use LLMs or any other streaming APIs, you might want to surface partial results during the execution of the tool, especially if the tool takes a longer time to run.\n",
"\n",
"This guide shows how to stream data from within a tool using the `astream` API with `stream_mode=\"messages\"` and also the more granular `astream_events` API. The `astream` API should be sufficient for most use cases.\n",
"1. To stream **arbitrary** data from inside a tool you can use [`stream_mode=\"custom\"`](../streaming#custom) and `get_stream_writer()`:\n",
"\n",
" ```python\n",
" # highlight-next-line\n",
" from langgraph.config import get_stream_writer\n",
" \n",
" def tool(tool_arg: str):\n",
" writer = get_stream_writer()\n",
" for chunk in custom_data_stream():\n",
" # stream any arbitrary data\n",
" # highlight-next-line\n",
" writer(chunk)\n",
" ...\n",
" \n",
" for chunk in graph.stream(\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"custom\"\n",
" ):\n",
" print(chunk)\n",
" ```\n",
"\n",
"2. To stream LLM tokens generated by a tool calling an LLM you can use [`stream_mode=\"messages\"`](../streaming#messages):\n",
"\n",
" ```python\n",
" from langgraph.graph import StateGraph, MessagesState\n",
" from langchain_openai import ChatOpenAI\n",
" \n",
" model = ChatOpenAI()\n",
" \n",
" def tool(tool_arg: str):\n",
" model.invoke(tool_arg)\n",
" ...\n",
" \n",
" def call_tools(state: MessagesState):\n",
" tool_call = get_tool_call(state)\n",
" tool_result = tool(**tool_call[\"args\"])\n",
" ...\n",
" \n",
" graph = (\n",
" StateGraph(MessagesState)\n",
" .add_node(call_tools)\n",
" ...\n",
" .compile()\n",
" \n",
" for msg, metadata in graph.stream(\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"messages\"\n",
" ):\n",
" print(msg)\n",
" ```\n",
"\n",
"!!! note \"Using without LangChain\"\n",
"\n",
" If you need to stream data from inside tools **without using LangChain**, you can use [`stream_mode=\"custom\"`](../streaming/#custom). Check out the [example below](#example-without-langchain) to learn more.\n",
"\n",
"!!! warning \"Async in Python < 3.11\"\n",
" \n",
" When using Python < 3.11 with async code, please ensure you manually pass the `RunnableConfig` through to the chat model when invoking it like so: `model.ainvoke(..., config)`.\n",
" The stream method collects all events from your nested code using a streaming tracer passed as a callback. In 3.11 and above, this is automatically handled via [contextvars](https://docs.python.org/3/library/contextvars.html); prior to 3.11, [asyncio's tasks](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) lacked proper `contextvar` support, meaning that the callbacks will only propagate if you manually pass the config through. We do this in the `call_model` function below.\n",
"\n",
"## Setup\n",
"\n",
@@ -55,8 +87,8 @@
},
{
"cell_type": "code",
"execution_count": 3,
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"execution_count": 1,
"id": "b364dfe2-010b-4588-8489-fb4d8be1f200",
"metadata": {},
"outputs": [],
"source": [
@@ -66,10 +98,18 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 2,
"id": "0cf6b41d-7fcb-40b6-9a72-229cdd00a094",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
@@ -98,77 +138,135 @@
},
{
"cell_type": "markdown",
"id": "e3d02ebb-c2e1-4ef7-b187-810d55139317",
"id": "b4ddc3ff-5620-48de-82f0-03b9137410cf",
"metadata": {},
"source": [
"## Define the graph\n",
"## Streaming custom data\n",
"\n",
"We'll use a prebuilt ReAct agent for this guide"
]
},
{
"cell_type": "markdown",
"id": "9378fd4a-69e4-49e2-b34c-a98a0505ea35",
"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>"
"We'll use a [prebuilt ReAct agent][langgraph.prebuilt.chat_agent_executor.create_react_agent] for this guide:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 3,
"id": "f1975577-a485-42bd-b0f1-d3e987faf52b",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.callbacks import Callbacks\n",
"from langchain_core.messages import HumanMessage\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.config import get_stream_writer\n",
"\n",
"\n",
"@tool\n",
"async def get_items(\n",
" place: str,\n",
" callbacks: Callbacks, # <--- Manually accept callbacks (needed for Python <= 3.10)\n",
") -> str:\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
" # Attention when using async, you should be invoking the LLM using ainvoke!\n",
" # If you fail to do so, streaming will not WORK.\n",
" return await llm.ainvoke(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": f\"Can you tell me what kind of items i might find in the following place: '{place}'. \"\n",
" \"List at least 3 such items separating them by a comma. And include a brief description of each item..\",\n",
" }\n",
" ],\n",
" {\"callbacks\": callbacks},\n",
" )\n",
"async def get_items(place: str) -> str:\n",
" \"\"\"Use this tool to list items one might find in a place you're asked about.\"\"\"\n",
" # highlight-next-line\n",
" writer = get_stream_writer()\n",
"\n",
" # this can be replaced with any actual streaming logic that you might have\n",
" items = [\"books\", \"penciles\", \"pictures\"]\n",
" for chunk in items:\n",
" # highlight-next-line\n",
" writer({\"custom_tool_data\": chunk})\n",
"\n",
" return \", \".join(items)\n",
"\n",
"\n",
"llm = ChatOpenAI(model_name=\"gpt-4o\")\n",
"llm = ChatOpenAI(model_name=\"gpt-4o-mini\")\n",
"tools = [get_items]\n",
"# contains `agent` (tool-calling LLM) and `tools` (tool executor) nodes\n",
"agent = create_react_agent(llm, tools=tools)"
]
},
{
"cell_type": "markdown",
"id": "15cb55cc-b59d-4743-b6a3-13db75414d2c",
"id": "fa96d572-d15f-4f00-b629-cf25e0b4dece",
"metadata": {},
"source": [
"## Using stream_mode=\"messages\"\n",
"Let's now invoke our agent with an input that requires a tool call:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "8ae5051c-53b9-4c53-87b2-d7263cda3b7b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'custom_tool_data': 'books'}\n",
"{'custom_tool_data': 'penciles'}\n",
"{'custom_tool_data': 'pictures'}\n"
]
}
],
"source": [
"inputs = {\n",
" \"messages\": [ # noqa\n",
" {\"role\": \"user\", \"content\": \"what items are in the office?\"}\n",
" ]\n",
"}\n",
"async for chunk in agent.astream(\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"custom\",\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "6d8fa9fc-19af-47d6-9031-ee1720c51aa2",
"metadata": {},
"source": [
"## Streaming LLM tokens"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "38eaf453-9773-424d-a110-9e1038a69805",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessageChunk\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"Using `stream_mode=\"messages\"` is a good option if you don't have any complex LCEL logic inside of nodes (or you don't need super granular progress from within the LCEL chain)."
"\n",
"@tool\n",
"async def get_items(\n",
" place: str,\n",
" # Manually accept config (needed for Python <= 3.10)\n",
" # highlight-next-line\n",
" config: RunnableConfig,\n",
") -> str:\n",
" \"\"\"Use this tool to list items one might find in a place you're asked about.\"\"\"\n",
" # Attention: when using async, you should be invoking the LLM using ainvoke!\n",
" # If you fail to do so, streaming will NOT work.\n",
" response = await llm.ainvoke(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": (\n",
" f\"Can you tell me what kind of items i might find in the following place: '{place}'. \"\n",
" \"List at least 3 such items separating them by a comma. And include a brief description of each item.\"\n",
" ),\n",
" }\n",
" ],\n",
" # highlight-next-line\n",
" config,\n",
" )\n",
" return response.content\n",
"\n",
"\n",
"tools = [get_items]\n",
"# contains `agent` (tool-calling LLM) and `tools` (tool executor) nodes\n",
"agent = create_react_agent(llm, tools=tools)"
]
},
{
@@ -176,78 +274,210 @@
"execution_count": 6,
"id": "4c9cdad3-3e9a-444f-9d9d-eae20b8d3486",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Certainly|!| Here| are| three| items| you| might| find| in| a| bedroom|:\n",
"\n",
"|1|.| **|Bed|**|:| The| central| piece| of| furniture| in| a| bedroom|,| typically| consisting| of| a| mattress| supported| by| a| frame|.| It| is| designed| for| sleeping| and| can| vary| in| size| from| twin| to| king|.| Beds| often| have| bedding|,| including| sheets|,| pillows|,| and| comfort|ers|,| to| enhance| comfort|.\n",
"\n",
"|2|.| **|D|resser|**|:| A| piece| of| furniture| with| drawers| used| for| storing| clothing| and| personal| items|.| Dress|ers| often| have| a| flat| surface| on| top|,| which| can| be| used| for| decorative| items|,| a| mirror|,| or| personal| accessories|.| They| help| keep| the| bedroom| organized| and| clutter|-free|.\n",
"\n",
"|3|.| **|Night|stand|**|:| A| small| table| or| cabinet| placed| beside| the| bed|,| used| for| holding| items| such| as| a| lamp|,| alarm| clock|,| books|,| or| personal| items|.| Night|stands| provide| convenience| for| easy| access| to| essentials| during| the| night|,| adding| functionality| and| style| to| the| bedroom| decor|.|"
]
}
],
"source": [
"final_message = \"\"\n",
"inputs = {\n",
" \"messages\": [ # noqa\n",
" {\"role\": \"user\", \"content\": \"what items are in the bedroom?\"}\n",
" ]\n",
"}\n",
"async for msg, metadata in agent.astream(\n",
" {\"messages\": [(\"human\", \"what items are on the shelf?\")]}, stream_mode=\"messages\"\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"messages\",\n",
"):\n",
" # Stream all messages from the tool node\n",
" if (\n",
" msg.content\n",
" and not isinstance(msg, HumanMessage)\n",
" isinstance(msg, AIMessageChunk)\n",
" and msg.content\n",
" # Stream all messages from the tool node\n",
" # highlight-next-line\n",
" and metadata[\"langgraph_node\"] == \"tools\"\n",
" and not msg.name\n",
" ):\n",
" print(msg.content, end=\"|\", flush=True)\n",
" # Final message should come from our agent\n",
" if msg.content and metadata[\"langgraph_node\"] == \"agent\":\n",
" final_message += msg.content"
" print(msg.content, end=\"|\", flush=True)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "81656193-1cbf-4721-a8df-0e316fd510e5",
"id": "d598d7e2-617d-4c06-bc9a-6a03d5f58499",
"metadata": {},
"source": [
"## Using stream events API\n",
"\n",
"For simplicity, the `get_items` tool doesn't use any complex LCEL logic inside it -- it only invokes an LLM.\n",
"\n",
"However, if the tool were more complex (e.g., using a RAG chain inside it), and you wanted to see more granular events from within the chain, then you can use the astream events API.\n",
"\n",
"The example below only illustrates how to invoke the API.\n",
"\n",
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">Use async for the astream events API</p>\n",
" <p>\n",
" You should generally be using `async` code (e.g., using `ainvoke` to invoke the llm) to be able to leverage the astream events API properly.\n",
" </p>\n",
"</div>"
"## Example without LangChain"
]
},
{
"cell_type": "markdown",
"id": "780ddcb6-63a7-4c83-a739-bafbe3cd135a",
"metadata": {},
"source": [
"You can also stream data from within tool invocations **without using LangChain**. Below example demonstrates how to do it for a graph with a single tool-executing node. We'll leave it as an exercise for the reader to [implement ReAct agent from scratch](../react-agent-from-scratch) without using LangChain."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "c3acdec9-0a24-4348-921e-435c8ea6f9fe",
"id": "3e8be67f-4bb8-4f14-9fdb-fc60340f3930",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"import json\n",
"\n",
"from typing import TypedDict\n",
"from typing_extensions import Annotated\n",
"from langgraph.graph import StateGraph, START\n",
"\n",
"from openai import AsyncOpenAI\n",
"\n",
"openai_client = AsyncOpenAI()\n",
"model_name = \"gpt-4o-mini\"\n",
"\n",
"\n",
"async def stream_tokens(model_name: str, messages: list[dict]):\n",
" response = await openai_client.chat.completions.create(\n",
" messages=messages, model=model_name, stream=True\n",
" )\n",
" role = None\n",
" async for chunk in response:\n",
" delta = chunk.choices[0].delta\n",
"\n",
" if delta.role is not None:\n",
" role = delta.role\n",
"\n",
" if delta.content:\n",
" yield {\"role\": role, \"content\": delta.content}\n",
"\n",
"\n",
"# this is our tool\n",
"async def get_items(place: str) -> str:\n",
" \"\"\"Use this tool to list items one might find in a place you're asked about.\"\"\"\n",
" # highlight-next-line\n",
" writer = get_stream_writer()\n",
" response = \"\"\n",
" async for msg_chunk in stream_tokens(\n",
" model_name,\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": (\n",
" \"Can you tell me what kind of items \"\n",
" f\"i might find in the following place: '{place}'. \"\n",
" \"List at least 3 such items separating them by a comma. \"\n",
" \"And include a brief description of each item.\"\n",
" ),\n",
" }\n",
" ],\n",
" ):\n",
" response += msg_chunk[\"content\"]\n",
" # highlight-next-line\n",
" writer(msg_chunk)\n",
"\n",
" return response\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list[dict], operator.add]\n",
"\n",
"\n",
"# this is the tool-calling graph node\n",
"async def call_tool(state: State):\n",
" ai_message = state[\"messages\"][-1]\n",
" tool_call = ai_message[\"tool_calls\"][-1]\n",
"\n",
" function_name = tool_call[\"function\"][\"name\"]\n",
" if function_name != \"get_items\":\n",
" raise ValueError(f\"Tool {function_name} not supported\")\n",
"\n",
" function_arguments = tool_call[\"function\"][\"arguments\"]\n",
" arguments = json.loads(function_arguments)\n",
"\n",
" function_response = await get_items(**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]}\n",
"\n",
"\n",
"graph = (\n",
" StateGraph(State) # noqa\n",
" .add_node(call_tool)\n",
" .add_edge(START, \"call_tool\")\n",
" .compile()\n",
")"
]
},
{
"cell_type": "markdown",
"id": "4e712d12-841c-4eac-a4d8-d01c73c86c8c",
"metadata": {},
"source": [
"Let's now invoke our graph with an AI message that contains a tool call:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "2c30c7b4-62df-4855-8219-d5e1a1a09be9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"|In| a| bedroom|,| you| might| find| the| following| items|:\n",
"Sure|!| Here| are| three| common| items| you| might| find| in| a| bedroom|:\n",
"\n",
"|1|.| **|Bed|**|:| The| central| piece| of| furniture| in| a| bedroom|,| typically| consisting| of| a| mattress| on| a| frame|,| where| people| sleep|.| It| often| includes| bedding| such| as| sheets|,| blankets|,| and| pillows| for| comfort|.\n",
"|1|.| **|Bed|**|:| The| focal| point| of| the| bedroom|,| a| bed| typically| consists| of| a| mattress| resting| on| a| frame|,| and| it| may| include| pillows| and| bedding|.| It| provides| a| comfortable| place| for| sleeping| and| resting|.\n",
"\n",
"|2|.| **|Ward|robe|**|:| A| large|,| tall| cupboard| or| fre|estanding| piece| of| furniture| used| for| storing| clothes|.| It| may| have| hanging| space|,| shelves|,| and| sometimes| drawers| for| organizing| garments| and| accessories|.\n",
"|2|.| **|D|resser|**|:| A| piece| of| furniture| with| multiple| drawers|,| a| dresser| is| used| for| storing| clothes|,| accessories|,| and| personal| items|.| It| often| has| a| flat| surface| that| may| be| used| to| display| decorative| items| or| a| mirror|.\n",
"\n",
"|3|.| **|Night|stand|**|:| A| small| table| or| cabinet| placed| beside| the| bed|,| used| for| holding| items| like| a| lamp|,| alarm| clock|,| books|,| or| personal| belongings| that| might| be| needed| during| the| night| or| early| morning|.||"
"|3|.| **|Night|stand|**|:| Also| known| as| a| bedside| table|,| a| night|stand| is| placed| next| to| the| bed| and| typically| holds| items| like| lamps|,| books|,| alarm| clocks|,| and| personal| belongings| for| convenience| during| the| night|.\n",
"\n",
"|These| items| contribute| to| the| functionality| and| comfort| of| the| bedroom| environment|.|"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"inputs = {\n",
" \"messages\": [\n",
" {\n",
" \"content\": None,\n",
" \"role\": \"assistant\",\n",
" \"tool_calls\": [\n",
" {\n",
" \"id\": \"1\",\n",
" \"function\": {\n",
" \"arguments\": '{\"place\":\"bedroom\"}',\n",
" \"name\": \"get_items\",\n",
" },\n",
" \"type\": \"function\",\n",
" }\n",
" ],\n",
" }\n",
" ]\n",
"}\n",
"\n",
"async for event in agent.astream_events(\n",
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom.\"}]}, version=\"v2\"\n",
"async for chunk in graph.astream(\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"custom\",\n",
"):\n",
" if (\n",
" event[\"event\"] == \"on_chat_model_stream\"\n",
" and event[\"metadata\"].get(\"langgraph_node\") == \"tools\"\n",
" ):\n",
" print(event[\"data\"][\"chunk\"].content, end=\"|\", flush=True)"
" print(chunk[\"content\"], end=\"|\", flush=True)"
]
}
],
@@ -267,7 +497,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
File diff suppressed because one or more lines are too long
@@ -0,0 +1,211 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c5889ca0-6feb-4864-a630-e97ccc2c587e",
"metadata": {},
"source": [
"# How to stream LLM tokens from specific nodes\n",
"\n",
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Streaming](../../concepts/streaming/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"A common use case when [streaming LLM tokens](../streaming-tokens) is to only stream them from specific nodes. To do so, you can use `stream_mode=\"messages\"` and filter the outputs by the `langgraph_node` field in the streamed metadata:\n",
"\n",
"```python\n",
"from langgraph.graph import StateGraph\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI()\n",
"\n",
"def node_a(state: State):\n",
" model.invoke(...)\n",
" ...\n",
"\n",
"def node_b(state: State):\n",
" model.invoke(...)\n",
" ...\n",
"\n",
"graph = (\n",
" StateGraph(State)\n",
" .add_node(node_a)\n",
" .add_node(node_b)\n",
" ...\n",
" .compile()\n",
" \n",
"for msg, metadata in graph.stream(\n",
" inputs,\n",
" # highlight-next-line\n",
" stream_mode=\"messages\"\n",
"):\n",
" # stream from 'node_a'\n",
" # highlight-next-line\n",
" if metadata[\"langgraph_node\"] == \"node_a\":\n",
" print(msg)\n",
"```\n",
"\n",
"!!! note \"Streaming from a specific LLM invocation\"\n",
"\n",
" If you need to instead filter streamed LLM tokens to a specific LLM invocation, check out [this guide](../streaming-tokens#filter-to-specific-llm-invocation)"
]
},
{
"cell_type": "markdown",
"id": "dcff85bd-8a5d-409e-93d4-e9242b5e976d",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "05157237-783c-49de-9f29-7dca3c285647",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "efd22cd2-3152-433b-ad50-65be8ace61d4",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "a0ce8c26-f38d-4bdb-89ff-b058e7560019",
"metadata": {},
"source": [
"## Example"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "419a3c71-7bf6-4656-99b8-b5d61f3f4bf1",
"metadata": {},
"outputs": [],
"source": [
"from typing import TypedDict\n",
"from langgraph.graph import START, StateGraph, MessagesState\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"class State(TypedDict):\n",
" topic: str\n",
" joke: str\n",
" poem: str\n",
"\n",
"\n",
"def write_joke(state: State):\n",
" topic = state[\"topic\"]\n",
" joke_response = model.invoke(\n",
" [{\"role\": \"user\", \"content\": f\"Write a joke about {topic}\"}]\n",
" )\n",
" return {\"joke\": joke_response.content}\n",
"\n",
"\n",
"def write_poem(state: State):\n",
" topic = state[\"topic\"]\n",
" poem_response = model.invoke(\n",
" [{\"role\": \"user\", \"content\": f\"Write a short poem about {topic}\"}]\n",
" )\n",
" return {\"poem\": poem_response.content}\n",
"\n",
"\n",
"graph = (\n",
" StateGraph(State)\n",
" .add_node(write_joke)\n",
" .add_node(write_poem)\n",
" # write both the joke and the poem concurrently\n",
" .add_edge(START, \"write_joke\")\n",
" .add_edge(START, \"write_poem\")\n",
" .compile()\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "fed84d5e-ba10-4324-a664-dca263951a33",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"In| shadows| soft|,| they| quietly| creep|,| \n",
"|Wh|isk|ered| wonders|,| in| dreams| they| leap|.| \n",
"|With| eyes| like| lantern|s|,| bright| and| wide|,| \n",
"|Myst|eries| linger| where| they| reside|.| \n",
"\n",
"|P|aws| that| pat|ter| on| silent| floors|,| \n",
"|Cur|led| in| sun|be|ams|,| they| seek| out| more|.| \n",
"|A| flick| of| a| tail|,| a| leap|,| a| p|ounce|,| \n",
"|In| their| playful| world|,| we| can't| help| but| bounce|.| \n",
"\n",
"|Guard|ians| of| secrets|,| with| gentle| grace|,| \n",
"|Each| little| me|ow|,| a| warm| embrace|.| \n",
"|Oh|,| the| joy| that| they| bring|,| so| pure| and| true|,| \n",
"|In| the| heart| of| a| cat|,| there's| magic| anew|.| |"
]
}
],
"source": [
"for msg, metadata in graph.stream(\n",
" {\"topic\": \"cats\"},\n",
" # highlight-next-line\n",
" stream_mode=\"messages\",\n",
"):\n",
" # highlight-next-line\n",
" if msg.content and metadata[\"langgraph_node\"] == \"write_poem\":\n",
" print(msg.content, end=\"|\", flush=True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
@@ -1,355 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b23ced4e-dc29-43be-9f94-0c36bb181b8a",
"metadata": {},
"source": [
"# How to stream LLM tokens (without LangChain LLMs)"
]
},
{
"cell_type": "markdown",
"id": "7044eeb8-4074-4f9c-8a62-962488744557",
"metadata": {},
"source": [
"In this example we will stream tokens from the language model powering an agent. We'll be using OpenAI client library directly, without using LangChain chat models. We will also use a ReAct agent as an example."
]
},
{
"cell_type": "markdown",
"id": "a37f60af-43ea-4aa6-847a-df8cc47065f5",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph openai"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0cf6b41d-7fcb-40b6-9a72-229cdd00a094",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "1c5bc618",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "e3d02ebb-c2e1-4ef7-b187-810d55139317",
"metadata": {},
"source": [
"## Define model, tools and graph"
]
},
{
"cell_type": "markdown",
"id": "3ba684f1-d46b-42e4-95cf-9685209a5992",
"metadata": {},
"source": [
"### Define a node that will call OpenAI API"
]
},
{
"cell_type": "code",
"execution_count": 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",
" # note: we're wrapping the response in ChatGenerationChunk so that we can stream this back using stream_mode=\"messages\"\n",
" chunk = ChatGenerationChunk(\n",
" message=AIMessageChunk(\n",
" content=delta.content,\n",
" )\n",
" )\n",
" llm_run_manager.on_llm_new_token(delta.content, chunk=chunk)\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 we can stream this back using stream_mode=\"messages\"\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",
"id": "3a3877e8-8ace-40d5-ad04-cbf21c6f3250",
"metadata": {},
"source": [
"### Define our tools and a tool-calling node"
]
},
{
"cell_type": "code",
"execution_count": 8,
"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]}"
]
},
{
"cell_type": "markdown",
"id": "6685898c-9a1c-4803-a492-bd70574ebe38",
"metadata": {},
"source": [
"### Define our graph"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Literal\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph, END, START\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.add_edge(START, \"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()"
]
},
{
"cell_type": "markdown",
"id": "d046e2ef-f208-4831-ab31-203b2e75a49a",
"metadata": {},
"source": [
"## Stream tokens"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "d6ed3df5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[{'name': 'get_items', 'args': {}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {'place': ''}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {'place': 'bed'}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {'place': 'bedroom'}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"[{'name': 'get_items', 'args': {'place': 'bedroom'}, 'id': 'call_h7g3jsgeRXIOUiaEC0VtM4EI', 'type': 'tool_call'}]\n",
"In| the| bedroom|,| you| have| socks|,| shoes|,| and| some| dust| b|unn|ies|.|"
]
}
],
"source": [
"from langchain_core.messages import AIMessageChunk\n",
"\n",
"first = True\n",
"async for msg, metadata in graph.astream(\n",
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]},\n",
" stream_mode=\"messages\",\n",
"):\n",
" if msg.content:\n",
" print(msg.content, end=\"|\", flush=True)\n",
"\n",
" if isinstance(msg, AIMessageChunk):\n",
" if first:\n",
" gathered = msg\n",
" first = False\n",
" else:\n",
" gathered = gathered + msg\n",
"\n",
" if msg.tool_call_chunks:\n",
" print(gathered.tool_calls)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
+547
View File
@@ -0,0 +1,547 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "76c4b04f-0c03-4321-9d40-38d12c59d088",
"metadata": {},
"source": [
"# How to stream"
]
},
{
"cell_type": "markdown",
"id": "15403cdb-441d-43af-a29f-fc15abe03dcc",
"metadata": {},
"source": [
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Streaming](../../concepts/streaming/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.\n",
"\n",
"LangGraph is built with first class support for streaming. There are several different ways to stream back outputs from a graph run:\n",
"\n",
"- `\"values\"`: Emit all values in the state after each step.\n",
"- `\"updates\"`: Emit only the node names and updates returned by the nodes after each step.\n",
" If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.\n",
"- `\"custom\"`: Emit custom data from inside nodes using `StreamWriter`.\n",
"- [`\"messages\"`](../streaming-tokens): Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes.\n",
"- `\"debug\"`: Emit debug events with as much information as possible for each step.\n",
"\n",
"You can stream outputs from the graph by using `graph.stream(..., stream_mode=<stream_mode>)` method, e.g.:\n",
"\n",
"=== \"Sync\"\n",
"\n",
" ```python\n",
" for chunk in graph.stream(inputs, stream_mode=\"updates\"):\n",
" print(chunk)\n",
" ```\n",
"\n",
"=== \"Async\"\n",
"\n",
" ```python\n",
" async for chunk in graph.astream(inputs, stream_mode=\"updates\"):\n",
" print(chunk)\n",
" ```\n",
"\n",
"You can also combine multiple streaming mode by providing a list to `stream_mode` parameter:\n",
"\n",
"=== \"Sync\"\n",
"\n",
" ```python\n",
" for chunk in graph.stream(inputs, stream_mode=[\"updates\", \"custom\"]):\n",
" print(chunk)\n",
" ```\n",
"\n",
"=== \"Async\"\n",
"\n",
" ```python\n",
" async for chunk in graph.astream(inputs, stream_mode=[\"updates\", \"custom\"]):\n",
" print(chunk)\n",
" ```"
]
},
{
"cell_type": "markdown",
"id": "9723cf76-6fe4-4b52-829f-3f28712ddcb7",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "427f8f66-7404-4c7d-a642-af5053b8b28f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "03310ce6-e21f-4378-93bf-dd273fdb3e9a",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "80399508-bad8-43b7-8ec9-4c06ad1774cc",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "be4adbb2-61e8-4bb7-942d-b4dc27ba71ac",
"metadata": {},
"source": [
"Let's define a simple graph with two nodes:"
]
},
{
"cell_type": "markdown",
"id": "f6d4c513-1006-4179-bba9-d858fc952169",
"metadata": {},
"source": [
"## Define graph"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "faeb5ce8-d383-4277-b0a8-322e713638e4",
"metadata": {},
"outputs": [],
"source": [
"from typing import TypedDict\n",
"from langgraph.graph import StateGraph, START\n",
"\n",
"\n",
"class State(TypedDict):\n",
" topic: str\n",
" joke: str\n",
"\n",
"\n",
"def refine_topic(state: State):\n",
" return {\"topic\": state[\"topic\"] + \" and cats\"}\n",
"\n",
"\n",
"def generate_joke(state: State):\n",
" return {\"joke\": f\"This is a joke about {state['topic']}\"}\n",
"\n",
"\n",
"graph = (\n",
" StateGraph(State)\n",
" .add_node(refine_topic)\n",
" .add_node(generate_joke)\n",
" .add_edge(START, \"refine_topic\")\n",
" .add_edge(\"refine_topic\", \"generate_joke\")\n",
" .compile()\n",
")"
]
},
{
"cell_type": "markdown",
"id": "f9b90850-85bf-4391-b6b7-22ad45edaa3b",
"metadata": {},
"source": [
"## Stream all values in the state (stream_mode=\"values\") {#values}"
]
},
{
"cell_type": "markdown",
"id": "d1ed60d4-cf78-4d4d-a660-6879539e168f",
"metadata": {},
"source": [
"Use this to stream **all values** in the state after each step."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "3daca06a-369b-41e5-8e4e-6edc4d4af3a7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'topic': 'ice cream'}\n",
"{'topic': 'ice cream and cats'}\n",
"{'topic': 'ice cream and cats', 'joke': 'This is a joke about ice cream and cats'}\n"
]
}
],
"source": [
"for chunk in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=\"values\",\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "adcb1bdb-f9fa-4d42-87ce-8e25d4290883",
"metadata": {},
"source": [
"## Stream state updates from the nodes (stream_mode=\"updates\") {#updates}"
]
},
{
"cell_type": "markdown",
"id": "44c55326-d077-4583-ae5b-396f45daf21c",
"metadata": {},
"source": [
"Use this to stream only the **state updates** returned by the nodes after each step. The streamed outputs include the name of the node as well as the update."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "eed7d401-37d1-4d15-b6dd-88956fff89e1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'refine_topic': {'topic': 'ice cream and cats'}}\n",
"{'generate_joke': {'joke': 'This is a joke about ice cream and cats'}}\n"
]
}
],
"source": [
"for chunk in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=\"updates\",\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "b9ed9c68-b7c5-4420-945d-84fa33fcf88f",
"metadata": {},
"source": [
"## Stream debug events (stream_mode=\"debug\") {#debug}"
]
},
{
"cell_type": "markdown",
"id": "94690715-f86c-42f6-be2d-4df82f6f9a96",
"metadata": {},
"source": [
"Use this to stream **debug events** with as much information as possible for each step. Includes information about tasks that were scheduled to be executed as well as the results of the task executions."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "cc6354f6-0c39-49cf-a529-b9c6c8713d7c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'type': 'task', 'timestamp': '2025-01-28T22:06:34.789803+00:00', 'step': 1, 'payload': {'id': 'eb305d74-3460-9510-d516-beed71a63414', 'name': 'refine_topic', 'input': {'topic': 'ice cream'}, 'triggers': ['start:refine_topic']}}\n",
"{'type': 'task_result', 'timestamp': '2025-01-28T22:06:34.790013+00:00', 'step': 1, 'payload': {'id': 'eb305d74-3460-9510-d516-beed71a63414', 'name': 'refine_topic', 'error': None, 'result': [('topic', 'ice cream and cats')], 'interrupts': []}}\n",
"{'type': 'task', 'timestamp': '2025-01-28T22:06:34.790165+00:00', 'step': 2, 'payload': {'id': '74355cb8-6284-25e0-579f-430493c1bdab', 'name': 'generate_joke', 'input': {'topic': 'ice cream and cats'}, 'triggers': ['refine_topic']}}\n",
"{'type': 'task_result', 'timestamp': '2025-01-28T22:06:34.790337+00:00', 'step': 2, 'payload': {'id': '74355cb8-6284-25e0-579f-430493c1bdab', 'name': 'generate_joke', 'error': None, 'result': [('joke', 'This is a joke about ice cream and cats')], 'interrupts': []}}\n"
]
}
],
"source": [
"for chunk in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=\"debug\",\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "6791da60-0513-43e6-b445-788dd81683bb",
"metadata": {},
"source": [
"## Stream LLM tokens ([stream_mode=\"messages\"](../streaming-tokens)) {#messages}"
]
},
{
"cell_type": "markdown",
"id": "1f45d68b-f7ca-4012-96cc-d276a143f571",
"metadata": {},
"source": [
"Use this to stream **LLM messages token-by-token** together with metadata for any LLM invocations inside nodes or tasks. Let's modify the above example to include LLM calls:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "efa787e1-be4d-433b-a1af-46a9c99ad8f3",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"def generate_joke(state: State):\n",
" # highlight-next-line\n",
" llm_response = llm.invoke(\n",
" # highlight-next-line\n",
" [\n",
" # highlight-next-line\n",
" {\"role\": \"user\", \"content\": f\"Generate a joke about {state['topic']}\"}\n",
" # highlight-next-line\n",
" ]\n",
" # highlight-next-line\n",
" )\n",
" return {\"joke\": llm_response.content}\n",
"\n",
"\n",
"graph = (\n",
" StateGraph(State)\n",
" .add_node(refine_topic)\n",
" .add_node(generate_joke)\n",
" .add_edge(START, \"refine_topic\")\n",
" .add_edge(\"refine_topic\", \"generate_joke\")\n",
" .compile()\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "c251f809-8922-46ea-bd5b-18264fcc523a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Why| did| the| cat| sit| on| the| ice| cream| cone|?\n",
"\n",
"|Because| it| wanted| to| be| a| \"|p|urr|-f|ect|\"| scoop|!| 🍦|🐱|"
]
}
],
"source": [
"for message_chunk, metadata in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=\"messages\",\n",
"):\n",
" if message_chunk.content:\n",
" print(message_chunk.content, end=\"|\", flush=True)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "b1912d72-7b68-4810-8b98-d7f3c35fbb6d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'langgraph_step': 2,\n",
" 'langgraph_node': 'generate_joke',\n",
" 'langgraph_triggers': ['refine_topic'],\n",
" 'langgraph_path': ('__pregel_pull', 'generate_joke'),\n",
" 'langgraph_checkpoint_ns': 'generate_joke:568879bc-8800-2b0d-a5b5-059526a4bebf',\n",
" 'checkpoint_ns': 'generate_joke:568879bc-8800-2b0d-a5b5-059526a4bebf',\n",
" 'ls_provider': 'openai',\n",
" 'ls_model_name': 'gpt-4o-mini',\n",
" 'ls_model_type': 'chat',\n",
" 'ls_temperature': 0.7}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"metadata"
]
},
{
"cell_type": "markdown",
"id": "0d1ebeda-4498-40e0-a30a-0844cb491425",
"metadata": {},
"source": [
"## Stream custom data (stream_mode=\"custom\") {#custom}"
]
},
{
"cell_type": "markdown",
"id": "e9ca56cc-d36e-4061-b1f6-9ade4e3e00a0",
"metadata": {},
"source": [
"Use this to stream custom data from inside nodes using [`StreamWriter`][langgraph.types.StreamWriter]."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "e3bf6a2b-afe3-4bd3-8474-57cccd994f23",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.types import StreamWriter\n",
"\n",
"\n",
"# highlight-next-line\n",
"def generate_joke(state: State, writer: StreamWriter):\n",
" # highlight-next-line\n",
" writer({\"custom_key\": \"Writing custom data while generating a joke\"})\n",
" return {\"joke\": f\"This is a joke about {state['topic']}\"}\n",
"\n",
"\n",
"graph = (\n",
" StateGraph(State)\n",
" .add_node(refine_topic)\n",
" .add_node(generate_joke)\n",
" .add_edge(START, \"refine_topic\")\n",
" .add_edge(\"refine_topic\", \"generate_joke\")\n",
" .compile()\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "2ecfb0b0-3311-46f5-9dc8-6c7853373792",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'custom_key': 'Writing custom data while generating a joke'}\n"
]
}
],
"source": [
"for chunk in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=\"custom\",\n",
"):\n",
" print(chunk)"
]
},
{
"cell_type": "markdown",
"id": "28e67f4d-fcab-46a8-93e2-b7bee30336c1",
"metadata": {},
"source": [
"## Configure multiple streaming modes {#multiple}"
]
},
{
"cell_type": "markdown",
"id": "01ff946a-f38d-42ad-bc71-a2621fab1b6c",
"metadata": {},
"source": [
"Use this to combine multiple streaming modes. The outputs are streamed as tuples `(stream_mode, streamed_output)`."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "bf4cab4b-356c-4276-9035-26974abe1efe",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Stream mode: updates\n",
"{'refine_topic': {'topic': 'ice cream and cats'}}\n",
"\n",
"\n",
"Stream mode: custom\n",
"{'custom_key': 'Writing custom data while generating a joke'}\n",
"\n",
"\n",
"Stream mode: updates\n",
"{'generate_joke': {'joke': 'This is a joke about ice cream and cats'}}\n",
"\n",
"\n"
]
}
],
"source": [
"for stream_mode, chunk in graph.stream(\n",
" {\"topic\": \"ice cream\"},\n",
" # highlight-next-line\n",
" stream_mode=[\"updates\", \"custom\"],\n",
"):\n",
" print(f\"Stream mode: {stream_mode}\")\n",
" print(chunk)\n",
" print(\"\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+1 -1
View File
@@ -3,4 +3,4 @@ hide_comments: true
title: Home
---
{!README.md!}
{!../README.md!}
+7
View File
@@ -0,0 +1,7 @@
# 🚀 Prebuilt Agents
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file.
[//]: # (This file is stub. Do not edit this file directly!)
[//]: # (1. Update the `packages.yml` file in the `docs/_scripts/third_party_page` directory.)
[//]: # (2. From the /docs directory, run `make build-prebuilt` to generate an updated version of this file for testing locally.)
+5
View File
@@ -0,0 +1,5 @@
::: langgraph.config
options:
members:
- get_store
- get_stream_writer
+9
View File
@@ -0,0 +1,9 @@
::: langgraph.pregel.Pregel
options:
members:
- stream
- astream
- invoke
- ainvoke
- update_state
- aupdate_state
@@ -0,0 +1,48 @@
# INVALID_LICENSE
This error is raised when license verification fails while attempting to start a self-hosted LangGraph Platform server. This error is specific to the LangGraph Platform and is not related to the open source libraries.
## When This Occurs
This error occurs when running a self-hosted deployment of LangGraph Platform without a valid enterprise license or API key.
## Troubleshooting
### Confirm deployment type
First, confirm the desired mode of deployment.
#### For Local Development
If you're just developing locally, you can use the lightweight in-memory server by running `langgraph dev`.
See the [local server](../../tutorials/langgraph-platform/local-server.md) docs for more information.
#### For Managed LangGraph Platform
If you would like a fast managed environment, consider the [Cloud SaaS](../../concepts/langgraph_cloud.md) deployment option. This requires no additional license key.
#### For Self-Hosted Lite (Limited Features)
If your deployment is unlikely to see more than 1 million node executions per year and don't need Crons and other enterprise features, consider the [Self-Hosted Lite](../../concepts/deployment_options.md#self-hosted-lite) deployment option.
You can deploy with Self-Hosted Lite by setting a valid `LANGSMITH_API_KEY` in your environment (e.g., in the `.env` file referenced by `langgraph.json`) and building a Docker image. The API key must be associated with an account on a **Plus** plan or greater.
#### For Self-Hosted Enterprise (Full Features)
For full self-hosting, set the `LANGGRAPH_CLOUD_LICENSE_KEY` environment variable. If you are interested in an enterprise license key, please contact the LangChain support team.
For more information on deployment options and their features, see the [Deployment Options](../../concepts/deployment_options.md) documentation.
### Confirm credentials
If you have confirmed that you would like to self-host LangGraph Platform, please verify your credentials.
#### For Self-Hosted Lite
1. Confirm that you have provided a working `LANGSMITH_API_KEY` environment variable in your deployment environment or `.env` file
2. Confirm the provided API key is associated with an account on a **Plus** or **Enterprise** plan (or equivalent)
#### For Self-Hosted Enterprise
1. Confirm that you have provided a working `LANGGRAPH_CLOUD_LICENSE_KEY` environment variable in your deployment environment or `.env` file
2. Confirm the key is still valid and has not surpassed its expiration date
+7 -1
View File
@@ -1,6 +1,6 @@
# Error reference
This page contains guides around resolving common errors you may find while building with LangChain.
This page contains guides around resolving common errors you may find while building with LangGraph.
Errors referenced below will have an `lc_error_code` property corresponding to one of the below codes when they are thrown in code.
- [GRAPH_RECURSION_LIMIT](./GRAPH_RECURSION_LIMIT.md)
@@ -8,3 +8,9 @@ Errors referenced below will have an `lc_error_code` property corresponding to o
- [INVALID_GRAPH_NODE_RETURN_VALUE](./INVALID_GRAPH_NODE_RETURN_VALUE.md)
- [MULTIPLE_SUBGRAPHS](./MULTIPLE_SUBGRAPHS.md)
- [INVALID_CHAT_HISTORY](./INVALID_CHAT_HISTORY.md)
## LangGraph Platform
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
- [INVALID_LICENSE](./INVALID_LICENSE.md)
@@ -17,6 +17,11 @@
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
???+ note "Support by deployment type"
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access.
## Setting up our project
@@ -22,7 +22,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain langsmith langchain_openai"
"%pip install -U langgraph langchain langsmith langchain_openai langchain_community"
]
},
{
@@ -496,16 +496,16 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[1massistant\u001b[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
"\u001b[1muser\u001b[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
"\u001b[1massistant\u001b[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
"\u001b[1muser\u001b[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
"\u001b[1massistant\u001b[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
"\u001b[1muser\u001b[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
"\u001b[1massistant\u001b[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
"\u001b[1muser\u001b[0m: I'm sorry, I cannot help with that.\n",
"\u001b[1massistant\u001b[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
"\u001b[1muser\u001b[0m: FINISHED\n"
"\u001B[1massistant\u001B[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
"\u001B[1muser\u001B[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
"\u001B[1massistant\u001B[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
"\u001B[1muser\u001B[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
"\u001B[1massistant\u001B[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
"\u001B[1muser\u001B[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
"\u001B[1massistant\u001B[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
"\u001B[1muser\u001B[0m: I'm sorry, I cannot help with that.\n",
"\u001B[1massistant\u001B[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
"\u001B[1muser\u001B[0m: FINISHED\n"
]
}
],
@@ -555,7 +555,6 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain.smith import RunEvalConfig\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
@@ -614,12 +613,10 @@
}
],
"source": [
"evaluation = RunEvalConfig(evaluators=[did_resist])\n",
"\n",
"result = client.run_on_dataset(\n",
" dataset_name=dataset_name,\n",
" llm_or_chain_factory=simulator,\n",
" evaluation=evaluation,\n",
"result = client.evaluate(\n",
" simulator,\n",
" data=dataset_name,\n",
" evaluators=[did_resist],\n",
")"
]
}

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