mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-29 03:09:45 +02:00
Compare commits
128
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d45253cee8 | ||
|
|
1377e3b6ba | ||
|
|
148cf52981 | ||
|
|
9010303245 | ||
|
|
3397d8908f | ||
|
|
e005f0472b | ||
|
|
18a1d60e45 | ||
|
|
5091d5e9fe | ||
|
|
da1fae0c72 | ||
|
|
7db81d72dc | ||
|
|
4ce387ee28 | ||
|
|
b902db686d | ||
|
|
16be48079e | ||
|
|
cf3a9ad63e | ||
|
|
547830ef24 | ||
|
|
d333e52fce | ||
|
|
15fe44ddf8 | ||
|
|
b6aff521e5 | ||
|
|
35ff9f5211 | ||
|
|
d690fc3e00 | ||
|
|
3325787af7 | ||
|
|
77a3cffa7f | ||
|
|
d1f2a6c518 | ||
|
|
b984584851 | ||
|
|
bce4545021 | ||
|
|
29c317887d | ||
|
|
84a0eca935 | ||
|
|
d9ed1ef52e | ||
|
|
fda79e00ce | ||
|
|
3163a60466 | ||
|
|
ac05955222 | ||
|
|
f3fe30380c | ||
|
|
2c5ddceeb1 | ||
|
|
98976e016a | ||
|
|
ea8025b719 | ||
|
|
735a76a16c | ||
|
|
a33437964c | ||
|
|
cb9405bcee | ||
|
|
bd2268404c | ||
|
|
25f88740c1 | ||
|
|
e9809ae9c1 | ||
|
|
69311a4135 | ||
|
|
bb3193c83e | ||
|
|
7a2eb614dc | ||
|
|
7fd6931200 | ||
|
|
cbca07e3db | ||
|
|
066525b335 | ||
|
|
a1dad43602 | ||
|
|
1e8f097656 | ||
|
|
db26c915a9 | ||
|
|
9b62280fc5 | ||
|
|
515ad8d7a6 | ||
|
|
d378f0e06a | ||
|
|
740870df65 | ||
|
|
6e20c9f3f9 | ||
|
|
530544234a | ||
|
|
f11d241482 | ||
|
|
e81979827f | ||
|
|
2064ea4793 | ||
|
|
16cfeff78c | ||
|
|
4321d337d6 | ||
|
|
d56e2545a6 | ||
|
|
9a3f96c459 | ||
|
|
cb509ad6a5 | ||
|
|
e479a2c643 | ||
|
|
0caae32a40 | ||
|
|
a6b8098548 | ||
|
|
0631f19ef9 | ||
|
|
e9b0bc4d3b | ||
|
|
eb19b80d13 | ||
|
|
e5ccdd91c2 | ||
|
|
871f15a630 | ||
|
|
32aa87d4b2 | ||
|
|
55a3352e9b | ||
|
|
6adaa6cf78 | ||
|
|
28172a2767 | ||
|
|
028137a51a | ||
|
|
c3847fae6c | ||
|
|
3daa4ae466 | ||
|
|
6a36f4bf91 | ||
|
|
04a0443742 | ||
|
|
3b224df566 | ||
|
|
0a2e2b4796 | ||
|
|
0e0b3ffac5 | ||
|
|
75509bd221 | ||
|
|
f997e5eafd | ||
|
|
6337c62dc7 | ||
|
|
59992fac7c | ||
|
|
28a7ca03a9 | ||
|
|
9fe61a42d2 | ||
|
|
59ae3b873c | ||
|
|
31ba5c41ac | ||
|
|
6ddbdd0638 | ||
|
|
3f7f52ccdc | ||
|
|
ee9a34f726 | ||
|
|
72ce34e3e6 | ||
|
|
098d5ec403 | ||
|
|
45ef425d85 | ||
|
|
8c192414a2 | ||
|
|
c0db7f4d09 | ||
|
|
072f2d43eb | ||
|
|
c02a74e221 | ||
|
|
15af8a9e6c | ||
|
|
1b257b4d0a | ||
|
|
384814036e | ||
|
|
d5b0ad2cf6 | ||
|
|
8d8e514924 | ||
|
|
a37c4d6f49 | ||
|
|
4b3e07b67a | ||
|
|
6ba61cc768 | ||
|
|
5a79210904 | ||
|
|
4a60eaf32f | ||
|
|
cf7c3e7fd1 | ||
|
|
141b53ee6e | ||
|
|
37e8e00f1f | ||
|
|
a0ec9017f2 | ||
|
|
80cef60405 | ||
|
|
46cba763be | ||
|
|
830e5f4550 | ||
|
|
55e9409b6e | ||
|
|
39e65a1a62 | ||
|
|
82148e9bf2 | ||
|
|
ed09a77d9f | ||
|
|
7e267897c9 | ||
|
|
d279902156 | ||
|
|
440158b969 | ||
|
|
0cf3a64d66 | ||
|
|
953e2907d4 |
@@ -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
|
||||
|
||||
@@ -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,42 @@ 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_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git" \
|
||||
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
|
||||
|
||||
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: 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 +116,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 +149,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 = (
|
||||
|
||||
@@ -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
@@ -178,4 +178,5 @@ Untitled*.ipynb
|
||||
|
||||
Chinook.db
|
||||
|
||||
libs/langgraph/out
|
||||
.vercel
|
||||
.turbo
|
||||
|
||||
@@ -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
|
||||
@@ -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).
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
site/
|
||||
docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt
|
||||
|
||||
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 _scripts/generate_llms_text.py 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
|
||||
poetry run pip install "git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
|
||||
poetry run python3 -m ipykernel install --name=python3
|
||||
npm install -g tslab
|
||||
poetry run tslab install --python=python3
|
||||
poetry run jupyter kernelspec list
|
||||
|
||||
|
||||
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
@@ -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>*
|
||||
```
|
||||
@@ -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)
|
||||
@@ -0,0 +1,75 @@
|
||||
import nock, { Definition } from "nock";
|
||||
import msgpack from "msgpack-lite";
|
||||
import zlib from "node:zlib";
|
||||
import fs from "node:fs/promises";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
// deno style imports here because we're running this in the deno jupyter kernel
|
||||
|
||||
interface NockCassetteData {
|
||||
hash: string;
|
||||
entries: Definition[];
|
||||
}
|
||||
|
||||
// Utility functions for compression & serialization
|
||||
function compressData(data: NockCassetteData, compressionLevel = 9): string {
|
||||
const packed = msgpack.encode(data);
|
||||
const compressed = zlib.deflateSync(packed, { level: compressionLevel });
|
||||
return compressed.toString("base64");
|
||||
}
|
||||
|
||||
function decompressData(compressedString: string): NockCassetteData {
|
||||
const decoded = Buffer.from(compressedString, "base64");
|
||||
const decompressed = zlib.inflateSync(decoded);
|
||||
return msgpack.decode(decompressed) as NockCassetteData;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-unused-vars
|
||||
class HashedCassette {
|
||||
private recording = true;
|
||||
|
||||
constructor(
|
||||
private readonly cassettePath: string,
|
||||
private readonly hash: string
|
||||
) {}
|
||||
|
||||
async enter() {
|
||||
try {
|
||||
const rawCassette = await fs.readFile(this.cassettePath, "utf-8");
|
||||
const data = decompressData(rawCassette);
|
||||
if (data.hash === this.hash) {
|
||||
this.recording = false;
|
||||
nock.disableNetConnect();
|
||||
nock.define(data.entries);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("ENOENT")) {
|
||||
this.recording = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
nock.recorder.rec({
|
||||
dont_print: true,
|
||||
output_objects: true,
|
||||
});
|
||||
}
|
||||
|
||||
async exit() {
|
||||
if (this.recording) {
|
||||
const entries = nock.recorder.play() as Definition[];
|
||||
const data = {
|
||||
hash: this.hash,
|
||||
entries,
|
||||
};
|
||||
const compressed = compressData(data);
|
||||
await fs.writeFile(this.cassettePath, compressed);
|
||||
} else {
|
||||
nock.enableNetConnect();
|
||||
nock.restore();
|
||||
nock.cleanAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import base64
|
||||
import os
|
||||
import zlib
|
||||
from types import TracebackType
|
||||
from typing import Optional, Any, Type
|
||||
|
||||
import msgpack
|
||||
import vcr
|
||||
|
||||
os.environ.pop("LANGCHAIN_TRACING_V2", None)
|
||||
custom_vcr = vcr.VCR()
|
||||
|
||||
|
||||
def compress_data(data: Any, compression_level: int = 9) -> str:
|
||||
packed = msgpack.packb(data, use_bin_type=True)
|
||||
compressed = zlib.compress(packed, level=compression_level)
|
||||
return base64.b64encode(compressed).decode("utf-8")
|
||||
|
||||
|
||||
def decompress_data(compressed_string: str) -> Any:
|
||||
decoded = base64.b64decode(compressed_string)
|
||||
decompressed = zlib.decompress(decoded)
|
||||
return msgpack.unpackb(decompressed, raw=False)
|
||||
|
||||
|
||||
class AdvancedCompressedSerializer:
|
||||
def serialize(self, cassette_dict: Any) -> str:
|
||||
return compress_data(cassette_dict)
|
||||
|
||||
def deserialize(self, cassette_string: str) -> Any:
|
||||
return decompress_data(cassette_string)
|
||||
|
||||
|
||||
custom_vcr.register_serializer("advanced_compressed", AdvancedCompressedSerializer())
|
||||
custom_vcr.serializer = "advanced_compressed"
|
||||
|
||||
|
||||
class HashedCassette:
|
||||
def __init__(self, cassette_path: str, hash_value: str) -> None:
|
||||
"""A context manager for using VCR cassettes with an embedded hash value.
|
||||
|
||||
Args:
|
||||
cassette_path (str): The file path of the cassette (independent of hash).
|
||||
hash_value (str): The expected hash value (e.g. a uuid string).
|
||||
|
||||
This class provides a context manager for using VCR cassettes with an embedded hash value.
|
||||
The hash value is used to ensure that the cassette matches the expected state, and if not,
|
||||
the cassette is removed or updated with the new hash value.
|
||||
"""
|
||||
self.cassette_path: str = cassette_path
|
||||
self.hash_value: str = hash_value
|
||||
self.vcr: vcr.VCR = custom_vcr
|
||||
self.cassette_context: Optional[Any] = None
|
||||
self.exited: bool = False
|
||||
|
||||
def __enter__(self) -> Any:
|
||||
self.exited: bool = False
|
||||
# Get the serializer instance from the VCR instance.
|
||||
serializer = self.vcr.serializers[self.vcr.serializer]
|
||||
# If the cassette file exists, check its embedded hash.
|
||||
if os.path.exists(self.cassette_path):
|
||||
with open(self.cassette_path, "r") as f:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception as e:
|
||||
os.remove(self.cassette_path)
|
||||
else:
|
||||
existing_hash = cassette_data.get("cassette_hash")
|
||||
if existing_hash != self.hash_value:
|
||||
os.remove(self.cassette_path)
|
||||
# Now enter the VCR cassette context.
|
||||
self.cassette_context = custom_vcr.use_cassette(
|
||||
self.cassette_path,
|
||||
filter_headers=["x-api-key", "authorization"],
|
||||
record_mode="once",
|
||||
serializer="advanced_compressed",
|
||||
)
|
||||
return self.cassette_context.__enter__()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]] = None,
|
||||
exc_val: Optional[BaseException] = None,
|
||||
exc_tb: Optional[TracebackType] = None,
|
||||
) -> Optional[bool]:
|
||||
if self.exited:
|
||||
return
|
||||
self.exited = True
|
||||
# Exit the VCR cassette context.
|
||||
result = self.cassette_context.__exit__(exc_type, exc_val, exc_tb)
|
||||
serializer = self.vcr.serializers[self.vcr.serializer]
|
||||
# If a cassette was recorded (or updated), open and update its hash.
|
||||
if os.path.exists(self.cassette_path):
|
||||
with open(self.cassette_path, "r") as f:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception as e:
|
||||
return result
|
||||
# Update the cassette data with the expected hash.
|
||||
if cassette_data.get("cassette_hash") != self.hash_value:
|
||||
cassette_data["cassette_hash"] = self.hash_value
|
||||
serialized_data = serializer.serialize(cassette_data)
|
||||
with open(self.cassette_path, "w") as f:
|
||||
f.write(serialized_data)
|
||||
return result
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Experimental script to generate consolidated llms text from the docs."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from 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)
|
||||
@@ -1,6 +1,8 @@
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
@@ -8,21 +10,45 @@ from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def __init__(self, rewrite_links: bool = True, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.rewrite_links = rewrite_links
|
||||
|
||||
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 self.rewrite_links:
|
||||
# We'll need to adjust the logic for this to keep markdown format
|
||||
# but link to markdown files rather than ipynb files.
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
else:
|
||||
# Keep format but replace the .ipynb extension with .md
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r"[\1](\2.md)",
|
||||
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
|
||||
if cell.source.startswith("%") or cell.source.startswith("!"):
|
||||
# update metadata to denote that it's not a python cell
|
||||
cell.metadata["language_info"] = {"name": "unknown"}
|
||||
|
||||
# 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:
|
||||
@@ -112,12 +138,111 @@ exporter = MarkdownExporter(
|
||||
],
|
||||
)
|
||||
|
||||
md_executable = MarkdownExporter(
|
||||
preprocessors=[
|
||||
ExtractAttachmentsPreprocessor,
|
||||
EscapePreprocessor(rewrite_links=False),
|
||||
],
|
||||
template_name="md_executable",
|
||||
extra_template_basedirs=[
|
||||
os.path.join(os.path.dirname(__file__), "notebook_convert_templates")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
nb.metadata.mode = mode
|
||||
if mode == "markdown":
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
else:
|
||||
body, _ = md_executable.from_notebook_node(nb)
|
||||
return body
|
||||
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
DOCS = HERE.parent / "docs"
|
||||
|
||||
|
||||
# Convert notebooks to markdown
|
||||
def _convert_notebooks(
|
||||
*,
|
||||
output_dir: Optional[Path] = None,
|
||||
replace: bool = False,
|
||||
pattern: str = "*.ipynb",
|
||||
) -> None:
|
||||
"""Converting notebooks."""
|
||||
if not output_dir and not replace:
|
||||
raise ValueError("Either --output_dir or --replace must be specified")
|
||||
|
||||
output_dir_path = DOCS if replace else Path(output_dir)
|
||||
notebooks = list(DOCS.rglob(pattern))
|
||||
|
||||
file_names = [notebook.name for notebook in notebooks]
|
||||
|
||||
for notebook in notebooks:
|
||||
markdown = convert_notebook(notebook, mode="exec")
|
||||
markdown_path = output_dir_path / notebook.relative_to(DOCS).with_suffix(".md")
|
||||
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(markdown_path, "w") as f:
|
||||
f.write(markdown)
|
||||
if replace:
|
||||
notebook.unlink(missing_ok=False)
|
||||
|
||||
if replace:
|
||||
# The regex will match markdown links that point to *.ipynb files.
|
||||
# It captures:
|
||||
# group(1): the link text (inside the square brackets)
|
||||
# group(2): the file path (without the trailing .ipynb)
|
||||
link_pattern = r"(?<!!)\[([^\]]+)\]\((?![^)]*//)([^)]+)\.ipynb\)"
|
||||
|
||||
def replace_link(match: re.Match) -> str:
|
||||
link_text = match.group(1)
|
||||
link_target = match.group(2)
|
||||
# Reconstruct the file name with the .ipynb extension.
|
||||
# For example, if link_target is "foo/bar", then linked_file becomes "bar.ipynb".
|
||||
linked_file = Path(link_target).name + ".ipynb"
|
||||
# Only update if the notebook was among those converted.
|
||||
if linked_file in file_names:
|
||||
# Change the extension from .ipynb to .md
|
||||
return f"[{link_text}]({link_target}.md)"
|
||||
# Otherwise, leave the original link intact.
|
||||
return match.group(0)
|
||||
|
||||
# Process all markdown files in the output directory.
|
||||
for path in output_dir_path.rglob("*.md"):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
new_content = re.sub(link_pattern, replace_link, content)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert notebooks to markdown")
|
||||
parser.add_argument(
|
||||
"--output_dir",
|
||||
default=None,
|
||||
help="Directory to output markdown files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--replace",
|
||||
action="store_true",
|
||||
help="Replace original notebooks with markdown files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
default="*.ipynb",
|
||||
help="Glob pattern to match notebooks to convert",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
_convert_notebooks(
|
||||
replace=args.replace,
|
||||
output_dir=args.output_dir,
|
||||
pattern=args.pattern,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mimetypes": {
|
||||
"text/markdown": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{#https://github.com/rdbisme/nbconvert/blob/master/share/jupyter/nbconvert/templates/markdown/index.md.j2#}
|
||||
{% extends 'markdown/index.md.j2' %}
|
||||
|
||||
{% block input %}
|
||||
```
|
||||
{%- if 'magics_language' in cell.metadata -%}
|
||||
{{ cell.metadata.magics_language}}
|
||||
{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
|
||||
{{ nb.metadata.language_info.name }} exec="on" source="above" session="1"
|
||||
{%- endif %}
|
||||
{{ cell.source}}
|
||||
```
|
||||
{% endblock input %}
|
||||
|
||||
{%- block traceback_line -%}
|
||||
{%- endblock traceback_line -%}
|
||||
|
||||
{%- block stream -%}
|
||||
{%- endblock stream -%}
|
||||
|
||||
{%- block data_text scoped -%}
|
||||
{%- endblock data_text -%}
|
||||
|
||||
{%- block data_html scoped -%}
|
||||
```html
|
||||
{{ output.data['text/html'] | safe }}
|
||||
```
|
||||
{%- endblock data_html -%}
|
||||
|
||||
{%- block data_jpg scoped -%}
|
||||

|
||||
{%- endblock data_jpg -%}
|
||||
|
||||
{%- block data_png scoped -%}
|
||||

|
||||
{%- endblock data_png -%}
|
||||
@@ -1,13 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from markdown import Markdown
|
||||
from pymdownx.superfences import SuperFencesException
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
import posixpath
|
||||
|
||||
from markdown_exec.hooks import SessionHistoryEntry
|
||||
|
||||
from notebook_convert import convert_notebook
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
from notebook_convert import convert_notebook
|
||||
from setup_vcr import load_postamble, load_preamble, _hash_string
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -15,6 +23,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 +64,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.
|
||||
|
||||
@@ -90,8 +139,8 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
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}```'
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
@@ -106,15 +155,200 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
def handle_vcr_setup(
|
||||
*,
|
||||
formatter: Callable,
|
||||
language: str,
|
||||
code: str,
|
||||
session: str,
|
||||
id: str,
|
||||
md: Markdown,
|
||||
**kwargs: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle VCR setup in markdown content if necessary."""
|
||||
try:
|
||||
if kwargs.get("extra", None) is None:
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: extra dict is required"
|
||||
)
|
||||
|
||||
if kwargs["extra"].get("path", None) is None:
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: path is required"
|
||||
)
|
||||
|
||||
document_filename = kwargs["extra"]["path"]
|
||||
|
||||
if session is None or session == "" and id is None or id == "":
|
||||
id = _hash_string(code)
|
||||
|
||||
if session is not None and session != "":
|
||||
logger.info(f"new session {session} on page {document_filename}")
|
||||
|
||||
cassette_prefix = document_filename.replace(".md", "").replace(os.path.sep, "_")
|
||||
|
||||
cassette_dir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "cassettes")
|
||||
)
|
||||
os.makedirs(cassette_dir, exist_ok=True)
|
||||
|
||||
# Build a unique cassette name.
|
||||
cassette_name = os.path.join(
|
||||
cassette_dir,
|
||||
f"{cassette_prefix}_{session if session else id}_{language}.msgpack.zlib",
|
||||
)
|
||||
|
||||
# Add context manager at start with explicit __enter__ and __exit__ calls
|
||||
|
||||
wrapped_lines = [
|
||||
load_preamble(language, code, cassette_name),
|
||||
code,
|
||||
]
|
||||
|
||||
if session is None or session == "":
|
||||
logger.info(
|
||||
f"no session, adding postamble for {language} in {document_filename}"
|
||||
)
|
||||
wrapped_lines.append(load_postamble(language))
|
||||
|
||||
transformed_source = "\n".join(wrapped_lines)
|
||||
return dict(
|
||||
transform_source=lambda code: (transformed_source, code),
|
||||
id=id,
|
||||
extra={},
|
||||
)
|
||||
except Exception as e:
|
||||
raise SuperFencesException(traceback.format_exc()) from e
|
||||
|
||||
|
||||
def handle_vcr_teardown(
|
||||
*,
|
||||
formatter: Callable,
|
||||
language: str,
|
||||
session: str,
|
||||
history: list[SessionHistoryEntry],
|
||||
):
|
||||
last_inputs = dict(history[-1].inputs)
|
||||
code = load_postamble(language)
|
||||
md = last_inputs["md"]
|
||||
html = False
|
||||
update_toc = False
|
||||
|
||||
document_filename = last_inputs.get("extra", {}).get("path", None)
|
||||
|
||||
if document_filename is None:
|
||||
logger.warning(f"no document filename found while tearing down {session}!")
|
||||
else:
|
||||
logger.info(f"tearing down {session} on {document_filename}")
|
||||
logger.info(traceback.format_stack())
|
||||
|
||||
kwargs = dict(
|
||||
code=code,
|
||||
session=session,
|
||||
id=f"{id}_vcr_end",
|
||||
md=md,
|
||||
html=html,
|
||||
update_toc=update_toc,
|
||||
extra={},
|
||||
)
|
||||
|
||||
# This doesn't actually render anything, we just call the formatter so it
|
||||
# executes in the same context as the session of which we're disposing.
|
||||
formatter(**kwargs)
|
||||
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
|
||||
import hashlib
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
preambles = {
|
||||
"python": "vcr_setup_preamble.py",
|
||||
"typescript": "nock_setup_preamble.ts",
|
||||
}
|
||||
|
||||
|
||||
def _get_python_cassette_init(cassette_name: str, hash_: str) -> str:
|
||||
return dedent(
|
||||
f"""
|
||||
_cassette = HashedCassette('{cassette_name}', '{hash_}')
|
||||
_cassette.__enter__()
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _get_typescript_cassette_init(cassette_name: str, hash_: str) -> str:
|
||||
return dedent(
|
||||
f"""
|
||||
const _cassette = new HashedCassette("{cassette_name}", "{hash_}");
|
||||
await _cassette.enter();
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _get_python_cassette_cleanup() -> str:
|
||||
return "_cassette.__exit__()"
|
||||
|
||||
|
||||
def _get_typescript_cassette_cleanup() -> str:
|
||||
return "await _cassette.exit();"
|
||||
|
||||
|
||||
preamble_inits = {
|
||||
"python": _get_python_cassette_init,
|
||||
"py": _get_python_cassette_init,
|
||||
"typescript": _get_typescript_cassette_init,
|
||||
"ts": _get_typescript_cassette_init,
|
||||
}
|
||||
|
||||
preamble_cleanups = {
|
||||
"python": _get_python_cassette_cleanup,
|
||||
"py": _get_python_cassette_cleanup,
|
||||
"typescript": _get_typescript_cassette_cleanup,
|
||||
"ts": _get_typescript_cassette_cleanup,
|
||||
}
|
||||
|
||||
|
||||
def load_preamble(language: str, code: str, cassette_name: str) -> str:
|
||||
"""Load the source code for the preamble for a given language."""
|
||||
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
||||
|
||||
preamble_path = os.path.join(_assets_dir, preambles[language])
|
||||
with open(preamble_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
hash_ = _hash_string(code)
|
||||
lines.append(preamble_inits[language](cassette_name, hash_))
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def load_postamble(language: str) -> str:
|
||||
"""Load the source code for the postamble for a given language."""
|
||||
|
||||
return preamble_cleanups[language]()
|
||||
|
||||
|
||||
def _hash_string(input_string: str) -> str:
|
||||
# Encode the input string to bytes
|
||||
encoded_string = input_string.encode("utf-8")
|
||||
# Create a SHA-256 hash object
|
||||
sha256_hash = hashlib.sha256(encoded_string)
|
||||
# Get the hexadecimal digest of the hash
|
||||
return sha256_hash.hexdigest()
|
||||
+137
@@ -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 you’re 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 0
|
||||
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)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
#!/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:
|
||||
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])
|
||||
|
||||
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,11 @@
|
||||
#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"
|
||||
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
eNrtVwtUFNcZRiRaX+cYj6UGja6r1ajMMrM7+0KpQRCWlwvsIqAiHWbu7g7MzgwzswsrQSMxRWtP7FofxzQ+geURBU1IUFFr9ZiKtiamitEoGo3RaNqqaIz1Qe8si4Fge5LWnpPTZs7ZnXvn/vd////9blm1GwgizbF9ttKsBASClOBEXFlWLYBCFxClJT4nkBwcVZlqtlgrXAJ9erJDkngxMiKC4GkVxwOWoFUk54xwYxGkg5Ai4JhngJ9NZR5Hec70WVSidAJRJOxAVEYq5pYoSQ7KYiU4URbBLRNFheQAiiJAwJegoFmFaFOGK5QCxwCZxiUCQVmaA784OQow8ic7LyE4JxOxcIrBtygJgHDCiY1gRAA/SMDJQ4MklyAzQVWo/I3jmIAOkof3M7e5WL/NMq/H40hFiZIlnH4CO5ByA6rJNBQQSYHmA2TKDBFA5WloAaeAlN2MsHGCk5DJVPI2nhAgP+hh0c+cF6DnBIkGnVOSljz+AWBdsg1zlayHlLdBR8h2dykLjaRZu7K0VPYODBAtAMpP7mfQnZLLywekBClLc0qrHYCgoOQVlQ5OlLz1PQPXQJAkgO4ELMlRkLt3m30BzYcrKGBjCAnUwWCxwO8Wb10BADxCMLQb+Dp3ebcTPM/QpN/QiHyRY7cGgovImvRerpODicBUYCXvrmjRw5JmqEl0QkSqB6YZq8BUuEGFbi9GRImgWQamDcIQUCkf719v7r7AE2QB5IQEUtjr69xc352GE71VKQRptvRgSQikw1tFCE4d/nb374KLlWgn8FbHpPYWF1j8WpxGhWEq444ejGWLvNv8r0j/P8019WACJMGDkBzk5d2M1nc5iwGsXXJ4KzQGTY0ARB4WD3jFB7dJLrGsEgYG/PFwdaCItpiTuiLaFhRaGQuD5N1rdbjCFWqdwgJ4hRpV4wpME6kxRsJBfIp1a0xAjPWJMdlhFQhWtMG4zOzKgWrS4WILAFUX88To75WjD62R1YdVioBinhMBEtDKuzULSe9sH0hC7NudqYZwgp1g6QV+sd5aOaiwXdBsY2AZVoTMEgpHnKK3Qq/D6gMrXf6ug3ahCIYiKLZLLgQS5pisOM8JEiICEjYnyeM9He4kiuUEi9JgWo0ORdGpsBhJxkUBiysvlnNCmeJUBS8AhiOo3cUIbBGAoZ00DIL/P9D4YN5gcDO6szeFxBUAVvTWaNDOZ193EgHIEmQzHjOqNMJnz5OJunipZRqjXr+7J5kIuilUoXOKO3uvB1hsQcWtxV3ECE15T4+Hk9w8TEdhOG606dUaLYnr9BqbDcU0uJqicCOOaRti4pAYgnQAxOLPNm91bPas6JSEmDoL5B3DcQU0WHmmT9/cXNKWm+eMwk35pri4wmQxPxGNp1KLkpOSdW6re47KlY+lR0uFSYVFbFEm5o73wFjp1XqNVm/U4QimQlWYCkPUcXhSIqnDZ7qyioutHqfJJGSrRI3HDTJTku2x5uQkIoPLt+djC2zpeJYUb7LG6RKzhTQcTy9MI2KxBfGZMAPpGaLgzKUTYjFzskFvSYPxhG03KmKqAmYibIpiVKAeEFgPiFwNeCTaVQ1TFZQ/C6JUPRvhVIUJnmxmlvFMhWUE0wnAN2zaFloCUbM4FpxeBX3gctNUVKZHlRprKMKZmOi47ILYNGy2h8pPIeK1CTPU5jzOYHWakrPMVl26ZO/mBLUWQ9CAH3QobvAnz9eq/5tavZuFdC9vxOw/m2AcWU5kaZvNZwECLCFvHclwLgr2dAH4YMzTo7O9jQbSqCEMQKen0DxcbTMgMzPTt3dxe9wMKuUDwX+WL/Z1nkCH+uwbs/xHQf6nL/x1dEjpOSt+iw596Upm1v1Rk2cc2Hyr9Xr5nIxaaWJGRkPtr8nQGyc/qt/7QkLFg7sDTS8vnzM9q/U1u3tam2X/iMPB6n5bPhsf9uYB/d33j5fn7T3/oG1i+fRDH5ffNV+rfXZJecmuxTP/gDbbh0tg7zDfjoEJYVH7D4yd+IV1Lf1G/crbd87V9Q+zbLrmWF3FOw8YfnmKWLXk6MlXs0ZO+iDmvYUhv8Lc1+4e26K6EfTc8tBTia+0tQ8cOzIC63espj74bMnnywrKDoUMrBjgujKnKfFPQeq4y2VTtNrRrbR7g3Ax8fLGwYtS76w92CR0ZM2942m/eb/52o5Hkdf1pjFOroZ5Z0T7uk/IqGb6vRGOtttnB046P6467NyQ9diF+vyRJ05kl/85dEnlpdC3kK+EQfbiwoejHlx/IK5ZcvCtO/NylLtPvE9+dsTaum7975YNTaytahDLz941r5sYNq/5Tumm0pv0z9fT4ZephZv/Sv1GGvoqXTmB37XvZWrpm7UbVB3xRwaNV91f9Hw8zU9yaQfEVrQ82nZhxYbtLUm1U6TWloWOTY2eVZU3J7edmzkp5MqFCH+Q+gYN3GZtiIIRe5r4LrjqqeC7cEX3jayLYbqREPBkgKchXAqguFySYP4VlKNlXKSUiXITOCKNccwqzrQVpTt4JinFlB1rT3dkfFvERwh2lxNqJUtTlszzY615cDwPorN5ylKlDLF6Kq9MkG0mmCLCIypEF8t6ehstG9HDmNxvo/IPKPg/RsFtQUN/wMHfMxzsI/04w3v61vccZvwXAECvO4BOr/9ud4Af/4/eATAj/n94B9AZnvodwJZHGWyYFjOqcTwPYj01ZcBBnpHQGfQUpbOh//QO8BSwJUlReuK7Ycvib2LL1KPsx+jQPdenhE6bu5i6l3Z/zajX558JcSwPGhK82Jc5jMxf+0l6yhv7O0ab3Ftvf3B0tM3dT9O+JuiF5oyCVN/x441tVWszV5zW/m1+x7SOvfce3b41efqnc+c/bMu5UtSkz/9wSnH70gkLa1SDm9qIxuT4MyEnni059ZIxJ7T9cj42cumapqsLsv9CRl6SXk+x0zpz+8XxzEdhw9/5xf2QoAvOnaWxZy4unxOju/finOHHtXV37g148Rl31JbGpatrJtW1mMJ8S/HKRycV0ypeG9Jve8h07e9PbWrJwgbP3i+NVcxs6v/50i9/stEZPHv91X3PuI6tv3HQuPbSmf5th5dNv7T6p1NSnksdZNkYrSzRbi8PuUBcPf/QknBv/7YV4NPn07mvgLvRcunCuHUb303zVrjH7ft7Q83ZvBOtLdVDTmIbqxbgh4+NLV+T+OWwHciUJG2/MV8Ufnhk7aE9WapbhyZEWAVTW59OVDixrOJnc4ODgv4Bu7XJtg==
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
eNrtVnlUFPcdR63GJg+8a+O5blJFZZadvRfqgbtyCSxyyCGKszO/2R3YOZiZZXc5DILGPDzXJh5NkxhEQB7gASKCmKRJqk+bxkg8yGHtU2uOGkVT4m1/sywGqq8vfc/+0dZ5j535ze/7+3yvz3z5lFbnA16gWGZAHcWIgMdwES6ETaXVPMhzAkFcWUUD0c4SlYmW5JQdTp7qnGEXRU4ICw3FOErBcoDBKAXO0qH5aChux8RQ+Mw5gA+m0soSns8GvFQop4EgYDYgyMNkiwvlOAt9MSJcyF3wyDRBJtqBzAUweONlFCMTSHmITM6zDiDZOAXAy4uXwDc0SwCH9MrGiYiGlYwYuEThXRB5gNFwQWIOAcAXIqA5mJDo5CUQpUIpvWNZhz8G0cP5wEkn48tZwnr4HCYrlDMY7TOwATHbH5pkQwAB5ynObyZPFQAMnoIZsDJo2ScJkuVpTDJTSMc4jId4sMKCD5zjYeV4kQI9S5wSPb4HwDilHBbLGQ8uHYOFkPLuDRYmSTE2eXGxVB3YIIoHhM/cB9DXkrXmAFyElsVLiqvtACOg53MBwyvtrCB6G/q3bjeG4wAWFDA4S0B8b72tgOJCZAQgHZgIamG7GOArjLc2FwAOwRxUPqjqOeXdg3Gcg8J9qYbmCCxT528vIsXy6Hat1E4EkoERvQcjBA+DW2AkETGhiR5INEaGKjQGhXKPGxFEjGIckDiIA4NBVXG+/ba+GxyG50IkxE9ib1XP4Ya+Nqzg3RmP4ZbkfpAYj9u9OzGe1mka+77nnYxI0cBbbUp81J1/80d3agWKKox7+wFLGXnrfbcw3y/FHugHAkTeg+AsxPK+razCWTaXAt7O69nZOJltpWflFESRImo2o3kmY3xaKr3AYcu0KBLjFqaSCsYakZroVhii9NqIGMNCBNWr9GqtAdWqEVShVKAKFElXavQ6nLXFJZvd7pSFeZp5pkQqN7/A6kqwuBx6PpbCWJeiIFbLi04nSeq4NHeCmgdUhDnKlJHNABOKWYEBJXW2eGcBkREJ5jGMzRUug9E58yliFpOftEhPGvhkLjUjzi0wCckuYx7rSTXYEjT2yHQxErXZouKiDersvuEZVQZE6Y9Qp9QYlNLV0EsUB2Bsot27Q21Q1/BA4ODoAGVVsGSiUyithKQEfzxa7R8hFZYFP/J5TKUZEtTbnmJ3hshUOlky4GQqpUojQ9VhGlWYWiuLik+pM/ndpDyWj3tTeIwRSMjJ+b38r8btTiYXELWmxzK/XWI+7KQUPpxRCHBzrAAQf1TeunQkqWd4IjHmxp7PDGF5G8ZQBT633l0SoeGwpJgm/zacBxIkdI7QAiyEBm3w7/RyrRbmpURQJaJED0pjAIfflxQ4x/IiIgAcjmbR4+0MoTG39HHNUsOq62CRw+Eowh1OAiQ7rWaWhj6FcBnHAweLEa1uBA5I4KBoCjbB9+sf+/CbQaUWtTxqIbK5gBG8NWplz3W4rwkPJA9SGg+BKo3wOvR4o14slWRj1Ota+5sJoE9AO3S00PLovh+iQinUuXuNEYrwdr4IF9katUanAoRSpUX1aozQqjGj0mpQW3EdasAwFN1tikRMGG4HSLKPbd5qc0ZCRHyMqTkd6UsbxOKb+HCfYQWGIsmqZMDD1nhrcQfrJOCc5EEVxEqKyPA2GXAj9GWEzNdAt5gRmZ+WtKcX7SHJKqUh6/sPuaKqZ65/OKBt8pqhAb5rEPx78EBMit/QoRx96HJa+tUvtv91QQaPjxg6YGCQ7O2dwfwblz9YcqT7s/XNC089WL+pKabZWrynTv+m50JX+OCVJTXbZe+O+yYjl7j53f2NRbdyJgX9fN3dO6/eu7uk7eNTUXfSU3+9+qN3T9wZpHoJ8rlVO7L9RE7ptvnf5CWuu26w7GsLXhC1/dSYYysWjZ9cFbb2QN44plEZq/gg8rmXg569Pk22cmNHy7BfpK6KCj6aL9Q3fTWhfkT5rZFTlu2fMGPuG6P+lHwJayyJm7khh6sPm55WciZz89rYg5o3T1POpXxF0uY5ty+HX/lb19fh3e+cm63tmsZMml0U3nA7/mT784FdCXuNb42ZfWbN6+TKkoorR+s/2fJxdHFZ98sJQxMXu5AZc9JXdwRtvXYvqHyi4XCZ+lDL8qD7rvttsb+XX53w2pZXFH+/8buDn+7fW3CVkf9M8SoXT3lbu6Z/ay7LPbB3mBg6O3DtqfqunWc21AzpDHnLjD2/a/Dak899tb+a33X5k9zKffMsH97cfSM4XrGqduyKKw6+aGbLOY9jgSJl3el0NCiQSz659QY9sqt4yHvGm89IHRoUUL0M/GEWbNeTlEwDdz4RyRQi63uQcTocfUwwOG7giIVbfmGUjWOOf6WOKElqyCWj7ESPy8AKvDvPAjypxjy7KprkMucnuH+qiMJ4m5OGUUne5IVZPvmSBZ+zoODJkhfLJdXSP3h5jJQz5nBhHkEmOBnG82jSUhL9ksn+KSE/FZZPheVTYfm/LSx1ev2TFZa6/1JhqdIq/w+Fpc7wxIWlWkMqgR4Qep1BaVRZSS2JEyQJgIrACUKj1/4nhaVebdVp/j1h6fxnYZkYkXBh7vBDd8bUG48Er6dH8mWvDX/mxbnHywfNO62kPv/NRMuwDhoZcOnqzPAp5LDCr02pabtqZ48KAPqcwGNNmXFjswo8+z9id08ur3Yd+/x21+r7Z79rdul/+MuVLSbt+NHft85/IbtWbVrCBW++u0I3Yv/5879Fxh4/lAWAceRE+kTgvqnRmYmtW0PLxaVVGctHNxOB+zLfFwYGXDOem1Lx5/Ntme8Xda8Sj9aNT/m2Y1RJ+cUhZoo8XtFe02ked710edy9s/K7EQgSuWfS8pxhZxYW4p3D04d0HHll6RdBLT8sd3V/OdX6ZeOkzJvnDr5weM4l/tOV0buLLlrcdPul5vKabXc3rlhVOq6zeOA1VffVB7sq51Uqon/1/fh7y1pmBOXnjCuSN17QxUY3vX5tzYPy2sDy5vXv0DVBt+rYqWW3pl88m9S1fnBbbfPh935ZMSsuc/WzN8C20EUZeQ2biq+P7ZGEE3nFqsUDAwL+AV1TiAE=
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
-1
@@ -1 +0,0 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
-1
File diff suppressed because one or more lines are too long
-1
@@ -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 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
@@ -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. You’re 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/) |
|
||||
@@ -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**.
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -51,7 +51,7 @@ For more information, please see:
|
||||
|
||||
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 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.
|
||||
|
||||
|
||||
@@ -5,11 +5,21 @@
|
||||
|
||||
## 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 +130,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 +140,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 +217,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 +528,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
|
||||
|
||||
|
||||
@@ -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
@@ -72,7 +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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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?
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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! |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ LangGraph is built with first class support for streaming. There are several dif
|
||||
`.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:
|
||||
|
||||
@@ -33,7 +33,7 @@ The below visualization shows the difference between the `values` and `updates`
|
||||
|
||||
## 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).
|
||||
In addition, you can use the `astream_events` 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`.
|
||||
|
||||
@@ -145,7 +145,7 @@ guide for that [here](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
|
||||
!!! warning "ASYNC IN PYTHON<=3.10"
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb).
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.
|
||||
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
{
|
||||
"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 typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"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
+134
-285
File diff suppressed because one or more lines are too long
@@ -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\"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
-21
@@ -9,16 +9,24 @@ Here you’ll 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
|
||||
|
||||
@@ -81,15 +89,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,7 +130,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)
|
||||
See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
|
||||
@@ -142,14 +145,15 @@ 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 (beta)
|
||||
[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).
|
||||
@@ -158,7 +162,7 @@ One of the big benefits of LangGraph is that you can easily create your own agen
|
||||
|
||||
These guides show how to use the prebuilt ReAct agent:
|
||||
|
||||
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
|
||||
- [How to use the pre-built ReAct agent](create-react-agent.md)
|
||||
- [How to add thread-level memory to a ReAct Agent](create-react-agent-memory.ipynb)
|
||||
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
|
||||
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
|
||||
@@ -299,3 +303,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)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 LangChain’s 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
@@ -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
@@ -3,4 +3,4 @@ hide_comments: true
|
||||
title: Home
|
||||
---
|
||||
|
||||
{!README.md!}
|
||||
{!../README.md!}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
[//]: # (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 you’re 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!)
|
||||
| Name | GitHub URL | Description | Weekly Downloads |
|
||||
| --- | --- | --- | --- |
|
||||
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 6976 |
|
||||
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 421 |
|
||||
|
||||
## ✨ 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](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) 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! 🚀
|
||||
@@ -0,0 +1,5 @@
|
||||
::: langgraph.config
|
||||
options:
|
||||
members:
|
||||
- get_store
|
||||
- get_stream_writer
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -12,7 +12,7 @@ Get started deploying your LangGraph applications locally or on the cloud with
|
||||
|
||||
## Deployment Options
|
||||
|
||||
- [Self-Hosted Lite](../concepts/self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Self-Hosted Lite](../concepts/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](../concepts/langgraph_cloud.md): Hosted as part of LangSmith.
|
||||
- [Bring Your Own Cloud](../concepts/bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
|
||||
- [Self-Hosted Enterprise](../concepts/self_hosted.md): Completely managed by you.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,18 +35,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "705d4020-6ee8-44cc-b1a5-8c34e7172fc7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_API_KEY: ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
@@ -1161,6 +1153,7 @@
|
||||
")\n",
|
||||
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
||||
"graph_builder.set_entry_point(\"chatbot\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = graph_builder.compile(checkpointer=memory)\n",
|
||||
"```\n",
|
||||
"</pre>\n",
|
||||
@@ -2472,7 +2465,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
" \"\"\"Read the specified document.\"\"\"\n",
|
||||
" with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n",
|
||||
" lines = file.readlines()\n",
|
||||
" if start is not None:\n",
|
||||
" if start is None:\n",
|
||||
" start = 0\n",
|
||||
" return \"\\n\".join(lines[start:end])\n",
|
||||
"\n",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"\n",
|
||||
"A single agent can usually operate effectively using a handful of tools within a single domain, but even using powerful models like `gpt-4`, it can be less effective at using many tools. \n",
|
||||
"\n",
|
||||
"One way to approach complicated tasks is through a \"divide-and-conquer\" approach: create an specialized agent for each task or domain and route tasks to the correct \"expert\". This is an example of a [multi-agent network](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#network) architecture.\n",
|
||||
"One way to approach complicated tasks is through a \"divide-and-conquer\" approach: create a specialized agent for each task or domain and route tasks to the correct \"expert\". This is an example of a [multi-agent network](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#network) architecture.\n",
|
||||
"\n",
|
||||
"This notebook (inspired by the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://arxiv.org/abs/2308.08155), by Wu, et. al.) shows one way to do this using LangGraph.\n",
|
||||
"\n",
|
||||
|
||||
+47
-23
@@ -56,20 +56,37 @@ plugins:
|
||||
- search:
|
||||
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
- autorefs
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
'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'
|
||||
- markdown-exec:
|
||||
ansi: required
|
||||
hooks:
|
||||
python:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
py:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
typescript:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
ts:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
import:
|
||||
- https://docs.python.org/3/objects.inv
|
||||
- https://api.python.langchain.com/en/latest/objects.inv
|
||||
- https://python.langchain.com/api_reference/objects.inv
|
||||
options:
|
||||
enable_inventory: true
|
||||
members_order: source
|
||||
allow_inspection: true
|
||||
heading_level: 2
|
||||
@@ -104,12 +121,20 @@ nav:
|
||||
- how-tos/index.md
|
||||
- LangGraph:
|
||||
- LangGraph: how-tos#langgraph
|
||||
- Graph API Basics:
|
||||
- Graph API Basics: how-tos#graph-api-basics
|
||||
- how-tos/state-reducers.ipynb
|
||||
- how-tos/sequence.ipynb
|
||||
- how-tos/branching.ipynb
|
||||
- how-tos/recursion-limit.ipynb
|
||||
- how-tos/visualization.ipynb
|
||||
- Controllability:
|
||||
- Controllability: how-tos#controllability
|
||||
- how-tos/branching.ipynb
|
||||
- how-tos/map-reduce.ipynb
|
||||
- how-tos/recursion-limit.ipynb
|
||||
- how-tos/command.ipynb
|
||||
- how-tos/configuration.ipynb
|
||||
- how-tos/node-retries.ipynb
|
||||
- how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- Persistence:
|
||||
- Persistence: how-tos#persistence
|
||||
- how-tos/persistence.ipynb
|
||||
@@ -138,15 +163,10 @@ nav:
|
||||
- how-tos/review-tool-calls-functional.ipynb
|
||||
- Streaming:
|
||||
- Streaming: how-tos#streaming
|
||||
- how-tos/stream-values.ipynb
|
||||
- how-tos/stream-updates.ipynb
|
||||
- how-tos/streaming.ipynb
|
||||
- how-tos/streaming-tokens.ipynb
|
||||
- how-tos/streaming-tokens-without-langchain.ipynb
|
||||
- how-tos/streaming-content.ipynb
|
||||
- how-tos/stream-multiple.ipynb
|
||||
- how-tos/streaming-specific-nodes.ipynb
|
||||
- how-tos/streaming-events-from-within-tools.ipynb
|
||||
- how-tos/streaming-events-from-within-tools-without-langchain.ipynb
|
||||
- how-tos/streaming-from-final-node.ipynb
|
||||
- how-tos/streaming-subgraphs.ipynb
|
||||
- how-tos/disable-streaming.ipynb
|
||||
- Tool calling:
|
||||
@@ -177,15 +197,13 @@ nav:
|
||||
- Other:
|
||||
- Other: how-tos#other
|
||||
- how-tos/async.ipynb
|
||||
- how-tos/visualization.ipynb
|
||||
- how-tos/configuration.ipynb
|
||||
- how-tos/node-retries.ipynb
|
||||
- how-tos/react-agent-structured-output.ipynb
|
||||
- how-tos/run-id-langsmith.ipynb
|
||||
- how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- how-tos/autogen-integration.ipynb
|
||||
- how-tos/autogen-integration-functional.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent.md
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
@@ -203,6 +221,7 @@ nav:
|
||||
- cloud/deployment/custom_docker.md
|
||||
- cloud/deployment/test_locally.md
|
||||
- cloud/deployment/graph_rebuild.md
|
||||
- how-tos/autogen-langgraph-platform.ipynb
|
||||
- Deployment:
|
||||
- Deployment: how-tos#deployment
|
||||
- cloud/deployment/cloud.md
|
||||
@@ -358,6 +377,8 @@ nav:
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- Resources:
|
||||
- Prebuilt Agents: prebuilt.md
|
||||
- Adopters: adopters.md
|
||||
- FAQ: concepts/faq.md
|
||||
- Troubleshooting:
|
||||
- Troubleshooting: troubleshooting/errors/index.md
|
||||
@@ -367,8 +388,9 @@ nav:
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- LangGraph Academy Course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||
|
||||
|
||||
- API reference:
|
||||
- Library:
|
||||
- Graphs: reference/graphs.md
|
||||
@@ -379,6 +401,8 @@ nav:
|
||||
- Errors: reference/errors.md
|
||||
- Types: reference/types.md
|
||||
- Constants: reference/constants.md
|
||||
- Pregel: reference/pregel.md
|
||||
- Config: reference/config.md
|
||||
- Functional API: reference/func.md
|
||||
- LangGraph Platform:
|
||||
- Server API: "cloud/reference/api/api_ref.md"
|
||||
|
||||
@@ -159,6 +159,24 @@
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.md-banner a {
|
||||
color: #000000;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.md-banner a:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Dark mode banner links */
|
||||
[data-md-color-scheme="slate"] .md-banner a {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-banner a:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* control the navbar depth */
|
||||
[data-md-level="2"] .md-nav {
|
||||
display: none;
|
||||
@@ -197,5 +215,5 @@
|
||||
|
||||
|
||||
{% block announce %}
|
||||
To learn more about LangGraph, check out our first LangChain Academy course, <em>Introduction to LangGraph</em>, available for free <a href="https://academy.langchain.com/courses/intro-to-langgraph">here</a>.
|
||||
<b>Join us at <a href="https://interrupt.langchain.com/" target="_blank" rel="noopener noreferrer"> Interrupt: The Agent AI Conference by LangChain</a> on May 13 & 14 in San Francisco!</b>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.38",
|
||||
"@langchain/openai": "^0.4.2",
|
||||
"msgpack-lite": "^0.1.26",
|
||||
"nock": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.8",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^22.13.1"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user