mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
257
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad14d92f5e | ||
|
|
277c341817 | ||
|
|
88d0f41c55 | ||
|
|
0a4953c4bc | ||
|
|
059e16789c | ||
|
|
41b36dcf3f | ||
|
|
b39dcd7fad | ||
|
|
c0ad92b6db | ||
|
|
b8d25bc0ed | ||
|
|
7884401ec8 | ||
|
|
a3bc029344 | ||
|
|
4875973ac5 | ||
|
|
15e67fdd57 | ||
|
|
1393270664 | ||
|
|
6e0295b4de | ||
|
|
4aadfccf95 | ||
|
|
70b2da1301 | ||
|
|
209864da45 | ||
|
|
8b6ef35f0c | ||
|
|
a01537d1a5 | ||
|
|
05b4a30c04 | ||
|
|
93e10fbe15 | ||
|
|
f6989f2c7d | ||
|
|
c4f8346479 | ||
|
|
82c9d4b368 | ||
|
|
647f22fdd9 | ||
|
|
0aba1b4887 | ||
|
|
4c0c52d996 | ||
|
|
580fe68c8e | ||
|
|
2f26268ff4 | ||
|
|
84f8a43f6e | ||
|
|
7ce0e3e15e | ||
|
|
1e6d958434 | ||
|
|
68ba8aa393 | ||
|
|
e6a0f08561 | ||
|
|
f38784a291 | ||
|
|
fc834127fd | ||
|
|
f3a0cbf294 | ||
|
|
f4fec76257 | ||
|
|
457641f15e | ||
|
|
9f15e15e26 | ||
|
|
cb9989030e | ||
|
|
a008725c06 | ||
|
|
a93f17e624 | ||
|
|
c7eddcc6e3 | ||
|
|
4cdad6c206 | ||
|
|
b9fb155d59 | ||
|
|
8b817a5b16 | ||
|
|
3a67f3a3eb | ||
|
|
1283539500 | ||
|
|
69ad42cac5 | ||
|
|
264b02e3ad | ||
|
|
b5c659bc9f | ||
|
|
4623f7b5da | ||
|
|
1641402341 | ||
|
|
d03ead2f43 | ||
|
|
5a8624fdfd | ||
|
|
c44ec55095 | ||
|
|
25d682cc9e | ||
|
|
6f37330141 | ||
|
|
1356a0ba42 | ||
|
|
9786be1ff7 | ||
|
|
c2a129c882 | ||
|
|
62f004fd28 | ||
|
|
d4b22ac1d4 | ||
|
|
0415c02b40 | ||
|
|
e8665f84e7 | ||
|
|
8ff5e79e70 | ||
|
|
7f4822931e | ||
|
|
9706211aca | ||
|
|
405da6d507 | ||
|
|
bf7252cadc | ||
|
|
e33bac6737 | ||
|
|
da97d2e1ba | ||
|
|
6baf320d8e | ||
|
|
a064ccdca1 | ||
|
|
b5479b48bf | ||
|
|
9dbcb03185 | ||
|
|
f2faa39ca9 | ||
|
|
b4f6cdf01f | ||
|
|
cd976e779d | ||
|
|
c1f337f50b | ||
|
|
31d3ceaf6d | ||
|
|
a48844632d | ||
|
|
437891aa4f | ||
|
|
3e1bbd3123 | ||
|
|
9111449ffd | ||
|
|
77d7c00ce8 | ||
|
|
91725d742d | ||
|
|
d73a4539ec | ||
|
|
15e2df6da5 | ||
|
|
ce6b396186 | ||
|
|
d2ab02edf1 | ||
|
|
0aef9424a8 | ||
|
|
ed23288e5b | ||
|
|
7ec8a4cb4d | ||
|
|
2d9ca3045e | ||
|
|
b065c54871 | ||
|
|
3c0a677c90 | ||
|
|
7e4852373d | ||
|
|
422b2ba7f0 | ||
|
|
2c66ac869d | ||
|
|
1ef7121100 | ||
|
|
a53287f3d8 | ||
|
|
da96925ecb | ||
|
|
f3403eab48 | ||
|
|
208d9d165d | ||
|
|
49c74dd569 | ||
|
|
2d97af57f8 | ||
|
|
b310ce07bc | ||
|
|
65976f311f | ||
|
|
80c9d61fbd | ||
|
|
a578c7b137 | ||
|
|
04e8342d97 | ||
|
|
b0e11ae524 | ||
|
|
d45253cee8 | ||
|
|
1377e3b6ba | ||
|
|
c36323cba8 | ||
|
|
148cf52981 | ||
|
|
9010303245 | ||
|
|
661476e88d | ||
|
|
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 | ||
|
|
0a861815b7 | ||
|
|
928126f2d7 | ||
|
|
0d8fea847a | ||
|
|
b4dc3a851f | ||
|
|
7568862013 | ||
|
|
69dc5f326e | ||
|
|
90f1e66748 | ||
|
|
a910a1a341 | ||
|
|
554e763994 | ||
|
|
adff1df813 | ||
|
|
64b828def6 | ||
|
|
9196cc2da8 |
@@ -20,7 +20,30 @@ env:
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
sdk-js: ${{ steps.filter.outputs.sdk-js }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
python:
|
||||
- 'libs/langgraph/**'
|
||||
- 'libs/sdk-py/**'
|
||||
- 'libs/cli/**'
|
||||
- 'libs/checkpoint/**'
|
||||
- 'libs/checkpoint-sqlite/**'
|
||||
- 'libs/checkpoint-postgres/**'
|
||||
- 'libs/scheduler-kafka/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
|
||||
lint:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -34,12 +57,14 @@ jobs:
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/scheduler-kafka",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_lint.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -50,6 +75,7 @@ jobs:
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_test.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
@@ -57,17 +83,23 @@ jobs:
|
||||
|
||||
# NOTE: we're testing langgraph separately because it requires a different matrix
|
||||
test-langgraph:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/langgraph"
|
||||
uses: ./.github/workflows/_test_langgraph.yml
|
||||
secrets: inherit
|
||||
|
||||
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
|
||||
test-scheduler-kafka:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/scheduler-kafka"
|
||||
uses: ./.github/workflows/_test_scheduler_kafka.yml
|
||||
secrets: inherit
|
||||
|
||||
check-sdk-methods:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "Check SDK methods matching"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -80,11 +112,15 @@ jobs:
|
||||
run: python .github/scripts/check_sdk_methods.py
|
||||
|
||||
integration-test:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: CLI integration test
|
||||
uses: ./.github/workflows/_integration_test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -109,6 +145,8 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
test-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -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,50 @@ jobs:
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
cache-key: docs
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: docs/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with test --no-root
|
||||
yarn
|
||||
poetry install --with test --with docs --no-root
|
||||
poetry run pip install -U \
|
||||
pytest \
|
||||
pytest-check-links \
|
||||
langsmith \
|
||||
langchain \
|
||||
GitPython \
|
||||
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
|
||||
|
||||
# we run this installation only for internal PRs
|
||||
# as GITHUB_TOKEN is not available for PRs from outside contributors
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then
|
||||
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
fi
|
||||
|
||||
poetry run jupyter kernelspec list
|
||||
poetry run python3 -m ipykernel install --user --name=python3
|
||||
npm install -g tslab
|
||||
poetry run tslab install --python=python3
|
||||
poetry run jupyter kernelspec list
|
||||
|
||||
- name: Run unit tests
|
||||
# Run unit tests on the docs build pipeline
|
||||
run: make tests
|
||||
- name: Lint Docs
|
||||
# This step lints the docs using the existing linting set up.
|
||||
# It should be very fast and should not require any external services.
|
||||
run: make lint-docs
|
||||
- name: Build llms-text
|
||||
run: make llms-text
|
||||
- name: Build site
|
||||
run: make build-docs
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
|
||||
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
||||
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
||||
- name: Check links in notebooks
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
@@ -95,14 +124,15 @@ jobs:
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
echo "Fetching changes from origin/main..."
|
||||
git fetch origin main
|
||||
echo "Checking for changed notebook files..."
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|docs/site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
if [ -n "${CHANGED_FILES}" ]; then
|
||||
echo "Running link check on HTML files matching changed notebook files..."
|
||||
@@ -127,7 +157,7 @@ jobs:
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/site/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import toml
|
||||
|
||||
pyproject_toml = toml.load("libs/langgraph/pyproject.toml")
|
||||
pyproject_toml = toml.load("pyproject.toml")
|
||||
|
||||
# Extract the ignore words list (adjust the key as per your TOML structure)
|
||||
ignore_words_list = (
|
||||
|
||||
@@ -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
|
||||
@@ -245,7 +242,7 @@ final_state["messages"][-1].content
|
||||
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
|
||||
</li>
|
||||
<li>
|
||||
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/modules/agents/tools/custom_tools">here</a>.
|
||||
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -294,7 +291,7 @@ Then we define one normal and one conditional edge. Conditional edge means that
|
||||
<ul>
|
||||
<li>
|
||||
When we compile the graph, we turn it into a LangChain
|
||||
<a href="https://python.langchain.com/v0.2/docs/concepts/#runnable-interface">Runnable</a>,
|
||||
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
|
||||
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
|
||||
with your inputs
|
||||
</li>
|
||||
@@ -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,66 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
|
||||
|
||||
build-typedoc:
|
||||
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-prebuilt:
|
||||
# Use to create an update to date prebuilt page.
|
||||
# Looks up download stats for each of the prebuilt packages and
|
||||
# generates the final prebuilt page.
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml
|
||||
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
poetry run python -m _scripts.generate_llms_text docs/llms-full.txt
|
||||
|
||||
install-vercel-deps:
|
||||
dnf install -y python3.11
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry self update 1.8.5
|
||||
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
|
||||
poetry env use /usr/bin/python3.11
|
||||
poetry install --with docs --with test --no-root
|
||||
|
||||
tests:
|
||||
# Run unit tests
|
||||
poetry run pytest tests/unit_tests
|
||||
|
||||
|
||||
vercel-build-docs: install-vercel-deps
|
||||
make build-docs
|
||||
|
||||
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs -name "*.ipynb" -type f -delete
|
||||
rm -rf site
|
||||
|
||||
## Run format against the project documentation.
|
||||
format-docs:
|
||||
poetry run ruff format docs
|
||||
poetry run ruff check --fix docs
|
||||
|
||||
# Check the docs for linting violations
|
||||
lint-docs:
|
||||
poetry run ruff format --check docs
|
||||
poetry run ruff check docs
|
||||
|
||||
codespell:
|
||||
./codespell_notebooks.sh .
|
||||
|
||||
start-services:
|
||||
docker compose -f test-compose.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f test-compose.yml down
|
||||
+7
-9
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import ast
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Literal, Optional
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional
|
||||
|
||||
import nbformat
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,14 +39,17 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
|
||||
([], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
|
||||
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
|
||||
(["langgraph.constants"], "langgraph.types", "Send", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Command", "types"),
|
||||
([], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
|
||||
(["langgraph.func"], "langgraph.func", "task", "func"),
|
||||
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
|
||||
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
|
||||
@@ -72,36 +69,19 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
|
||||
}
|
||||
|
||||
|
||||
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
|
||||
if not pkg_prefix.isidentifier():
|
||||
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
|
||||
return re.compile(
|
||||
r"from\s+(" + pkg_prefix + "(?:_\w+)?(?:\.\w+)*?)\s+import\s+"
|
||||
r"((?:\w+(?:,\s*)?)*" # Match zero or more words separated by a comma+optional ws
|
||||
r"(?:\s*\(.*?\))?)", # Match optional parentheses block
|
||||
re.DOTALL, # Match newlines as well
|
||||
)
|
||||
|
||||
|
||||
# Regular expression to match langchain import lines
|
||||
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
|
||||
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
|
||||
|
||||
|
||||
|
||||
|
||||
@lru_cache(maxsize=10_000)
|
||||
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
"""Get full module name using inspect, with LRU cache to memoize results."""
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
class_ = getattr(module, class_name)
|
||||
module = inspect.getmodule(class_)
|
||||
if module is None:
|
||||
# For constants, inspect.getmodule() might return None
|
||||
# In this case, we'll return the original module_path
|
||||
symbol = getattr(module, class_name)
|
||||
# First check the __module__ attribute on the symbol.
|
||||
mod_name = getattr(symbol, "__module__", None)
|
||||
# If __module__ is not set or comes from typing,
|
||||
# assume the definition is in module_path.
|
||||
if mod_name is None or mod_name.startswith("typing"):
|
||||
return module_path
|
||||
return module.__name__
|
||||
return mod_name
|
||||
except AttributeError as e:
|
||||
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
|
||||
return None
|
||||
@@ -109,139 +89,129 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
|
||||
return None
|
||||
|
||||
def _get_doc_title(data: str, file_name: str) -> str:
|
||||
try:
|
||||
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
pass
|
||||
# Parse the rst-style titles
|
||||
try:
|
||||
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
return file_name
|
||||
|
||||
|
||||
class ImportInformation(TypedDict):
|
||||
imported: str # The name of the class that was imported.
|
||||
source: str # The full module path from which the class was imported.
|
||||
docs: str # The URL pointing to the class's documentation.
|
||||
title: str # The title of the document where the import is used.
|
||||
path: str # The path of the file where the markdown content originated.
|
||||
|
||||
|
||||
def _get_imports(
|
||||
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
|
||||
) -> List[ImportInformation]:
|
||||
"""Get imports from the given code block.
|
||||
|
||||
Args:
|
||||
code: Python code block from which to extract imports
|
||||
doc_title: Title of the document
|
||||
package_ecosystem: "langchain" or "langgraph". The two live in different
|
||||
repositories and have separate documentation sites.
|
||||
|
||||
Returns:
|
||||
List of import information for the given code block
|
||||
"""
|
||||
imports = []
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pattern = _IMPORT_LANGCHAIN_RE
|
||||
elif package_ecosystem == "langgraph":
|
||||
pattern = _IMPORT_LANGGRAPH_RE
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
for import_match in pattern.finditer(code):
|
||||
module = import_match.group(1)
|
||||
if "pydantic_v1" in module:
|
||||
continue
|
||||
imports_str = (
|
||||
import_match.group(2).replace("(\n", "").replace("\n)", "")
|
||||
) # Handle newlines within parentheses
|
||||
# remove any newline and spaces, then split by comma
|
||||
imported_classes = [
|
||||
imp.strip()
|
||||
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
|
||||
if imp.strip()
|
||||
]
|
||||
for class_name in imported_classes:
|
||||
module_path = _get_full_module_name(module, class_name)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"title": doc_title,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
|
||||
def get_imports(code: str, path: str) -> List[ImportInformation]:
|
||||
"""Retrieve all import references from the given code for specified ecosystems.
|
||||
|
||||
Args:
|
||||
code: The source code from which to extract import references.
|
||||
doc_title: The documentation title associated with the code.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
A list of import information for each import found.
|
||||
"""
|
||||
ecosystems = ["langchain", "langgraph"]
|
||||
all_imports = []
|
||||
for package_ecosystem in ecosystems:
|
||||
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
|
||||
return all_imports
|
||||
# Parse the code into an AST.
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
found_imports = []
|
||||
|
||||
# Walk through the AST and process ImportFrom nodes.
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
# node.module is the source module.
|
||||
if node.module is None:
|
||||
continue
|
||||
for alias in node.names:
|
||||
if not (
|
||||
node.module.startswith("langchain")
|
||||
or node.module.startswith("langgraph")
|
||||
):
|
||||
continue
|
||||
|
||||
found_imports.append(
|
||||
{
|
||||
"source": node.module,
|
||||
# alias.name is the original name even if an alias exists.
|
||||
"imported": alias.name,
|
||||
}
|
||||
)
|
||||
|
||||
imports: list[ImportInformation] = []
|
||||
|
||||
for found_import in found_imports:
|
||||
module = found_import["source"]
|
||||
|
||||
if module.startswith("langchain"):
|
||||
# Handles things like `langchain` or `langchain_anthropic`
|
||||
package_ecosystem = "langchain"
|
||||
elif module.startswith("langgraph"):
|
||||
package_ecosystem = "langgraph"
|
||||
else:
|
||||
continue
|
||||
|
||||
class_name = found_import["imported"]
|
||||
module_path = _get_full_module_name(module, class_name)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"path": path,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def update_markdown_with_imports(markdown: str) -> str:
|
||||
def update_markdown_with_imports(markdown: str, path: str) -> str:
|
||||
"""Update markdown to include API reference links for imports in Python code blocks.
|
||||
|
||||
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
|
||||
This function scans the markdown content for Python code blocks, extracts any
|
||||
imports, and appends links to their API documentation.
|
||||
|
||||
Args:
|
||||
markdown: The markdown content to process.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
Updated markdown with API reference links appended to Python code blocks.
|
||||
@@ -252,10 +222,12 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
```python
|
||||
from langchain.nlp import TextGenerator
|
||||
```
|
||||
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
|
||||
This function will append an API reference link to the `TextGenerator` class
|
||||
from the `langchain.nlp` module if it's recognized.
|
||||
"""
|
||||
code_block_pattern = re.compile(
|
||||
r'(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```', re.DOTALL
|
||||
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def replace_code_block(match: re.Match) -> str:
|
||||
@@ -267,9 +239,8 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
Returns:
|
||||
str: The modified code block with API reference links appended if applicable.
|
||||
"""
|
||||
indent = match.group('indent')
|
||||
code_block = match.group('code')
|
||||
language = match.group('language') # Preserve the language from the regex match
|
||||
indent = match.group("indent")
|
||||
code_block = match.group("code")
|
||||
# Retrieve import information from the code block
|
||||
imports = get_imports(code_block, "__unused__")
|
||||
|
||||
@@ -279,12 +250,12 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
return original_code_block
|
||||
|
||||
# Generate API reference links for each import
|
||||
api_links = ' | '.join(
|
||||
api_links = " | ".join(
|
||||
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
|
||||
)
|
||||
# Return the code block with appended API reference links
|
||||
return f'{original_code_block}\n\n{indent}API Reference: {api_links}'
|
||||
return f"{original_code_block}\n\n{indent}API Reference: {api_links}"
|
||||
|
||||
# Apply the replace_code_block function to all matches in the markdown
|
||||
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
|
||||
return updated_markdown
|
||||
return updated_markdown
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Experimental script to generate consolidated llms text from the docs."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from _scripts.notebook_hooks import _on_page_markdown_with_config
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get source directory (parent of HERE / docs)
|
||||
SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(HERE), "docs"))
|
||||
|
||||
|
||||
def _make_llms_text(output_file: str) -> str:
|
||||
"""Generate a consolidated text file from markdown/notebook files for LLM training.
|
||||
|
||||
Args:
|
||||
output_file: Path to output the consolidated text file
|
||||
"""
|
||||
# Collect all markdown and notebook files
|
||||
relative_paths = [
|
||||
# Files relative to docs/docs/
|
||||
"tutorials/introduction.ipynb",
|
||||
]
|
||||
all_files = [os.path.join(SOURCE_DIR, path) for path in relative_paths]
|
||||
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.ipynb"), recursive=True)
|
||||
)
|
||||
# Add all concepts
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.ipynb"), recursive=True)
|
||||
)
|
||||
|
||||
all_content = []
|
||||
|
||||
# Process each file
|
||||
for file_path in all_files:
|
||||
print(f"Processing {file_path}")
|
||||
rel_path = os.path.relpath(file_path, SOURCE_DIR)
|
||||
|
||||
# Create File and Page objects to match mkdocs structure
|
||||
file_obj = File(
|
||||
path=rel_path, src_dir=SOURCE_DIR, dest_dir="", use_directory_urls=True
|
||||
)
|
||||
page = Page(
|
||||
title="",
|
||||
file=file_obj,
|
||||
config={},
|
||||
)
|
||||
|
||||
# Read raw content
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Convert to markdown without logic to resolve API references
|
||||
processed_content = _on_page_markdown_with_config(
|
||||
content, page, add_api_references=False, remove_base64_images=True
|
||||
)
|
||||
if processed_content:
|
||||
# Add file name
|
||||
all_content.append(f"---\n{rel_path}\n---")
|
||||
# Add content
|
||||
all_content.append(processed_content)
|
||||
|
||||
# Write consolidated output
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write("\n\n".join(all_content))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Generate consolidated text file from markdown/notebook files for LLMs."
|
||||
)
|
||||
)
|
||||
parser.add_argument("output_file", help="Path to output the consolidated text file")
|
||||
|
||||
args = parser.parse_args()
|
||||
_make_llms_text(args.output_file)
|
||||
@@ -1,28 +1,266 @@
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
|
||||
def _uses_input(source: str) -> bool:
|
||||
"""Parse the source code to determine if it uses the input() function."""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
# If there's a syntax error, assume input() might be present to be safe.
|
||||
return False
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call):
|
||||
# Check if the function called is named 'input'
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "input":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _rewrite_cell_magic(code: str) -> str:
|
||||
"""Process a code block that uses cell magic.:w
|
||||
|
||||
- Lines starting with "%%capture" are ignored.
|
||||
- Lines starting with "%pip" are rewritten by removing the leading "%" character.
|
||||
- Any other non-empty line causes a NotImplementedError.
|
||||
|
||||
Args:
|
||||
code (str): The original code block.
|
||||
|
||||
Returns:
|
||||
str: The transformed code block.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If a line doesn't start with either "%%capture" or "%pip".
|
||||
"""
|
||||
rewritten_lines = []
|
||||
|
||||
for line in code.splitlines():
|
||||
stripped = line.strip()
|
||||
# Skip empty lines
|
||||
if not stripped:
|
||||
continue
|
||||
# Ignore %%capture lines
|
||||
if stripped.startswith("%%capture"):
|
||||
continue
|
||||
# Rewrite %pip lines by dropping the '%'
|
||||
elif stripped.startswith("%pip"):
|
||||
# Drop the leading '%' character
|
||||
rewritten_lines.append(stripped[1:])
|
||||
# Anything else is not supported
|
||||
else:
|
||||
raise NotImplementedError(f"Unhandled line: {line}")
|
||||
|
||||
return "\n".join(rewritten_lines)
|
||||
|
||||
|
||||
class PrintCallVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
This visitor sets self.has_print to True if it encounters a call
|
||||
to a print within the global scope.
|
||||
|
||||
This should catch calls to print(), print_stream(), etc. (Prefixed with "print").
|
||||
|
||||
May have some false positives, but it's not meant to be perfect.
|
||||
|
||||
Temporary code for notebook conversion.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.has_print = False
|
||||
self.scope_level = 0 # counter to track whether we're inside a def/lambda
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Lambda(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Call(self, node):
|
||||
# Only consider calls when not inside a function definition.
|
||||
if self.scope_level == 0:
|
||||
if isinstance(node.func, ast.Name) and node.func.id.startswith("print"):
|
||||
self.has_print = True
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def _has_output(source: str) -> bool:
|
||||
"""Determine if the code block is expected to produce output.
|
||||
|
||||
Args:
|
||||
source (str): The source code of the code block.
|
||||
|
||||
Returns:
|
||||
True if the code block is expected to produce output, False otherwise.
|
||||
|
||||
Must meet the following conditions:
|
||||
|
||||
1. There is a call to a printing function (name starts with "print")
|
||||
that is not inside a function definition.
|
||||
2. The last top-level statement is an expression that is valid if:
|
||||
- It is any expression (including calls) AND
|
||||
- It is NOT a call to `display(...)`.
|
||||
|
||||
`display` isn't handled currently by markdown-exec
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return False
|
||||
|
||||
# Condition (1): Check for a global print-like call.
|
||||
visitor = PrintCallVisitor()
|
||||
visitor.visit(tree)
|
||||
condition_a = visitor.has_print
|
||||
|
||||
# Condition (2): Check the last top-level statement.
|
||||
condition_b = False
|
||||
if tree.body:
|
||||
last_stmt = tree.body[-1]
|
||||
if isinstance(last_stmt, ast.Expr):
|
||||
# If the expression is a call, ensure it's not a call to "display"
|
||||
if isinstance(last_stmt.value, ast.Call):
|
||||
if (
|
||||
isinstance(last_stmt.value.func, ast.Name)
|
||||
and last_stmt.value.func.id == "display"
|
||||
):
|
||||
condition_b = False # exclude display-wrapped expressions
|
||||
else:
|
||||
condition_b = True
|
||||
else:
|
||||
# Any other expression qualifies.
|
||||
condition_b = True
|
||||
|
||||
return condition_a or condition_b
|
||||
|
||||
|
||||
def _convert_links_in_markdown(markdown: str) -> str:
|
||||
"""Convert links present in notebook markdown cells to standardized format.
|
||||
|
||||
We want to update markdown links code cells by linking to markdown
|
||||
files rather than assuming that the link is to the finalized HTML.
|
||||
|
||||
This code is needed temporarily since the markdown links that are present
|
||||
in ipython notebooks do not follow the same conventions as regular markdown
|
||||
files in mkdocs (which should link to a .md file).
|
||||
"""
|
||||
|
||||
# Define the regex pattern in parts for clarity:
|
||||
pattern = (
|
||||
r"(?<!!)" # Negative lookbehind: ensure the link is not an image (i.e., doesn't start with "!")
|
||||
r"\[" # Literal '[' indicating the start of the link text.
|
||||
r"(?P<text>[^\]]*)" # Named group 'text': match any characters except ']', representing the link text.
|
||||
r"\]" # Literal ']' indicating the end of the link text.
|
||||
r"\(" # Literal '(' indicating the start of the URL.
|
||||
r"(?![^\)]*//)" # Negative lookahead: ensure that the URL does not contain '//' (skip absolute URLs).
|
||||
r"(?P<url>[^)]*)" # Named group 'url': match any characters except ')', representing the URL.
|
||||
r"\)" # Literal ')' indicating the end of the URL.
|
||||
)
|
||||
|
||||
def custom_replacement(match):
|
||||
"""logic will correct the link format used in ipython notebooks
|
||||
|
||||
Ipython notebooks were being converted directly into HTML links
|
||||
instead of markdown links that retain the markdown extension.
|
||||
|
||||
It needs to handle the following cases:
|
||||
- optional fragments (e.g., `#section`)
|
||||
e.g., `[text](url/#section)` -> `[text](url.md#section)`
|
||||
e.g., `[text](url#section)` -> `[text](url.md#section)`
|
||||
- relative paths (e.g., `../path/to/file`) need to be denested by 1 level
|
||||
"""
|
||||
text = match.group("text")
|
||||
url = match.group("url")
|
||||
|
||||
if url.startswith("../"):
|
||||
# we strip the "../" from the start of the URL
|
||||
# We only need to denest one level.
|
||||
url = url[3:]
|
||||
|
||||
url = url.rstrip("/") # Strip `/` from the end of the URL
|
||||
|
||||
# if url has a fragment
|
||||
if "#" in url:
|
||||
url, fragment = url.split("#")
|
||||
url = url.rstrip("/")
|
||||
# Strip `/` from the end of the URL
|
||||
return f"[{text}]({url}.md#{fragment})"
|
||||
# Otherwise add the .md extension
|
||||
return f"[{text}]({url}.md)"
|
||||
|
||||
return re.sub(
|
||||
pattern,
|
||||
custom_replacement,
|
||||
markdown,
|
||||
)
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.markdown_exec_migration = markdown_exec_migration
|
||||
|
||||
def preprocess_cell(self, cell, resources, cell_index):
|
||||
if cell.cell_type == "markdown":
|
||||
# rewrite markdown links to html links (excluding image links)
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
if not self.markdown_exec_migration:
|
||||
# Old logic is to convert ipynb links to HTML links
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
else:
|
||||
cell.source = _convert_links_in_markdown(cell.source)
|
||||
|
||||
# Fix image paths in <img> tags
|
||||
cell.source = re.sub(
|
||||
r'<img\s+src="\.?/img/([^"]+)"', r'<img src="../img/\1"', cell.source
|
||||
)
|
||||
|
||||
elif cell.cell_type == "code":
|
||||
# Determine if the cell has bash or cell magic
|
||||
source = cell.source
|
||||
is_exec = not (
|
||||
source.startswith("%") or source.startswith("!") or _uses_input(source)
|
||||
)
|
||||
cell.metadata["exec"] = is_exec
|
||||
|
||||
if self.markdown_exec_migration:
|
||||
# For markdown exec migration we'll re-write cell magic as bash commands
|
||||
if source.startswith("%%"):
|
||||
cell.source = _rewrite_cell_magic(source)
|
||||
cell.metadata["language"] = "shell"
|
||||
|
||||
cell.metadata["has_output"] = _has_output(source)
|
||||
|
||||
# Remove noqa comments
|
||||
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
|
||||
# escape ``` in code
|
||||
# This is needed because the markdown exporter will wrap code blocks in
|
||||
# triple backticks, which will break the markdown output if the code block
|
||||
# contains triple backticks.
|
||||
cell.source = cell.source.replace("```", r"\`\`\`")
|
||||
# escape ``` in output
|
||||
if "outputs" in cell:
|
||||
@@ -115,9 +353,11 @@ exporter = MarkdownExporter(
|
||||
|
||||
def convert_notebook(
|
||||
notebook_path: Path,
|
||||
) -> Path:
|
||||
mode: Literal["markdown", "exec"] = "markdown",
|
||||
) -> str:
|
||||
with open(notebook_path) as f:
|
||||
nb = nbformat.read(f, as_version=4)
|
||||
|
||||
nb.metadata.mode = mode
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
return body
|
||||
|
||||
+154
-19
@@ -1,13 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from notebook_convert import convert_notebook
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -15,6 +16,24 @@ logger.setLevel(logging.INFO)
|
||||
DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
||||
|
||||
|
||||
REDIRECT_MAP = {
|
||||
# lib redirects
|
||||
"how-tos/stream-values.ipynb": "how-tos/streaming.ipynb#values",
|
||||
"how-tos/stream-updates.ipynb": "how-tos/streaming.ipynb#updates",
|
||||
"how-tos/streaming-content.ipynb": "how-tos/streaming.ipynb#custom",
|
||||
"how-tos/stream-multiple.ipynb": "how-tos/streaming.ipynb#multiple",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming-tokens.ipynb#example-without-langchain",
|
||||
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "concepts/index.md#langgraph-platform",
|
||||
"cloud/how-tos/index.md": "how-tos/index.md#langgraph-platform",
|
||||
"cloud/concepts/api.md": "concepts/langgraph_server.md",
|
||||
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
|
||||
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
|
||||
}
|
||||
|
||||
|
||||
class NotebookFile(File):
|
||||
def is_documentation_page(self):
|
||||
return True
|
||||
@@ -38,6 +57,29 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
|
||||
return new_files
|
||||
|
||||
|
||||
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
|
||||
"""Add the path to the code blocks."""
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
|
||||
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
|
||||
r"(?P=indent)```" # Match closing backticks with the same indentation
|
||||
)
|
||||
|
||||
def replace_code_block_header(match: re.Match) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
if 'exec="on"' not in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
code = match.group("code")
|
||||
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
|
||||
|
||||
return code_block_pattern.sub(replace_code_block_header, markdown)
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
@@ -52,7 +94,7 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# existing hl_lines for Python and JavaScript
|
||||
# Pattern to find code blocks with highlight comments, handling optional indentation
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
|
||||
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
|
||||
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
|
||||
r"(?P=indent)```" # Match closing backticks with the same indentation
|
||||
)
|
||||
@@ -61,6 +103,13 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
code_block = match.group("code")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
# Account for a case where hl_lines is manually specified
|
||||
if "hl_lines" in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
lines = code_block.split("\n")
|
||||
highlighted_lines = []
|
||||
|
||||
@@ -86,35 +135,121 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# Reconstruct the new code block
|
||||
new_code_block = "\n".join(lines_to_keep)
|
||||
|
||||
# Construct the full code block that also includes
|
||||
# the fenced code block syntax.
|
||||
opening_fence = f"```{language}"
|
||||
|
||||
if attributes:
|
||||
opening_fence += f" {attributes}"
|
||||
|
||||
if highlighted_lines:
|
||||
return (
|
||||
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f'{new_code_block}'
|
||||
f'{indent}```'
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"{indent}```{language}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
|
||||
|
||||
return (
|
||||
# The indent and opening fence
|
||||
f"{indent}{opening_fence}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
|
||||
# Replace all code blocks in the markdown
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
def _on_page_markdown_with_config(
|
||||
markdown: str,
|
||||
page: Page,
|
||||
*,
|
||||
add_api_references: bool = True,
|
||||
remove_base64_images: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
if DISABLED:
|
||||
return markdown
|
||||
|
||||
if page.file.src_path.endswith(".ipynb"):
|
||||
logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
# logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
markdown = convert_notebook(page.file.abs_src_path)
|
||||
|
||||
# Append API reference links to code blocks
|
||||
markdown = update_markdown_with_imports(markdown)
|
||||
if add_api_references:
|
||||
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Add file path as an attribute to code blocks that are executable.
|
||||
# This file path is used to associate fixtures with the executable code
|
||||
# which can be used in CI to test the docs without making network requests.
|
||||
markdown = _add_path_to_code_blocks(markdown, page)
|
||||
|
||||
if remove_base64_images:
|
||||
# Remove base64 encoded images from markdown
|
||||
markdown = re.sub(r"!\[.*?\]\(data:image/+;base64,[^\)]+\)", "", markdown)
|
||||
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
return _on_page_markdown_with_config(
|
||||
markdown,
|
||||
page,
|
||||
add_api_references=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# redirects
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<link rel="canonical" href="{url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url={url}">
|
||||
</head>
|
||||
<body>
|
||||
Redirecting...
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def write_html(site_dir, old_path, new_path):
|
||||
"""Write an HTML file in the site_dir with a meta redirect to the new page"""
|
||||
# Determine all relevant paths
|
||||
old_path_abs = os.path.join(site_dir, old_path)
|
||||
old_dir_abs = os.path.dirname(old_path_abs)
|
||||
|
||||
# Create parent directories if they don't exist
|
||||
if not os.path.exists(old_dir_abs):
|
||||
os.makedirs(old_dir_abs)
|
||||
|
||||
# Write the HTML redirect file in place of the old file
|
||||
content = HTML_TEMPLATE.format(url=new_path)
|
||||
with open(old_path_abs, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
for page_old, page_new in REDIRECT_MAP.items():
|
||||
page_old = page_old.replace(".ipynb", ".md")
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
old_html_path = File(page_old, "", "", use_directory_urls).dest_path.replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
new_html_path = (
|
||||
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
import nbformat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
|
||||
NOTEBOOK_DIRS = ("docs/how-tos","docs/tutorials")
|
||||
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")
|
||||
|
||||
@@ -19,36 +19,37 @@ BLOCKLIST_COMMANDS = (
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_CASSETTES = (
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/how-tos/many-tools.ipynb"
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/how-tos/many-tools.ipynb"
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_EXECUTION = [
|
||||
# this uses a user provided project name for langsmith
|
||||
"docs/docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
"docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
# this uses langsmith datasets
|
||||
"docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
"docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
# this uses browser APIs
|
||||
"docs/docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
"docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
# these RAG guides use an ollama model
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
# this loads a massive dataset from gcp
|
||||
"docs/docs/tutorials/usaco/usaco.ipynb",
|
||||
"docs/tutorials/usaco/usaco.ipynb",
|
||||
# TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
|
||||
"docs/docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration-functional.ipynb",
|
||||
# TODO: need to update these notebooks to make sure they are runnable in CI
|
||||
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/docs/tutorials/tot/tot.ipynb",
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
"docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/tutorials/tot/tot.ipynb",
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
]
|
||||
|
||||
|
||||
@@ -216,7 +217,7 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {notebook_path}: {e}")
|
||||
|
||||
with open(os.path.join(DOCS_PATH, "notebooks_no_execution.json"), "w") as f:
|
||||
with open("notebooks_no_execution.json", "w") as f:
|
||||
json.dump(NOTEBOOKS_NO_EXECUTION, f)
|
||||
|
||||
|
||||
|
||||
+137
@@ -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 "-"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
library_list="\n".join(rows), langgraph_url=langgraph_url
|
||||
)
|
||||
return markdown_content
|
||||
|
||||
|
||||
def main(input_file: str, output_file: str, language: str) -> None:
|
||||
"""Main function to create the third party page.
|
||||
|
||||
Args:
|
||||
input_file: Path to the input YAML file containing resolved package information.
|
||||
output_file: Path to the output file for the third party page.
|
||||
language: The language for which to generate the third party page.
|
||||
"""
|
||||
# Parse the input YAML file
|
||||
with open(input_file, "r") as f:
|
||||
resolved_packages: List[ResolvedPackage] = yaml.safe_load(f)
|
||||
|
||||
markdown_content = generate_markdown(resolved_packages, language)
|
||||
|
||||
# Write the markdown content to the output file
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Create the third party page.")
|
||||
parser.add_argument(
|
||||
"input_file",
|
||||
help="Path to the input YAML file containing resolved package information.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file", help="Path to the output file for the third party page."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
choices=["python", "js"],
|
||||
default="python",
|
||||
help="The language for which to generate the third party page. Defaults to 'python'.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.input_file, args.output_file, args.language)
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
"""Retrieve download count for a list of Python packages from PyPI."""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
class Package(TypedDict):
|
||||
"""A TypedDict representing a package"""
|
||||
|
||||
name: str
|
||||
"""The name of the package."""
|
||||
repo: str
|
||||
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
class ResolvedPackage(Package):
|
||||
weekly_downloads: int | None
|
||||
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
PACKAGES_FILE = HERE / "packages.yml"
|
||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
|
||||
|
||||
|
||||
def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
for package in packages:
|
||||
# First check if package exists on PyPI
|
||||
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
|
||||
try:
|
||||
pypi_response = requests.get(pypi_url)
|
||||
pypi_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
|
||||
|
||||
# Get first release date
|
||||
pypi_data = pypi_response.json()
|
||||
releases = pypi_data["releases"]
|
||||
first_release_date = None
|
||||
for version_releases in releases.values():
|
||||
if version_releases: # Some versions may be empty lists
|
||||
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
|
||||
if first_release_date is None or upload_time < first_release_date:
|
||||
first_release_date = upload_time
|
||||
|
||||
if first_release_date is None:
|
||||
raise AssertionError(f"Package {package['name']} has no releases yet")
|
||||
|
||||
# If package was published in last 48 hours, skip download stats
|
||||
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
else:
|
||||
num_downloads = None
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
"repo": package["repo"],
|
||||
"weekly_downloads": num_downloads,
|
||||
"description": package["description"],
|
||||
}
|
||||
)
|
||||
|
||||
return resolved_packages
|
||||
|
||||
|
||||
|
||||
def main(output_file: str) -> None:
|
||||
"""Main function to generate package download information.
|
||||
|
||||
Args:
|
||||
output_file: Path to the output YAML file.
|
||||
"""
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES)
|
||||
|
||||
if not output_file.endswith(".yml"):
|
||||
raise ValueError("Output file must have a .yml extension")
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
f.write("# This file is auto-generated. Do not edit.\n")
|
||||
yaml.dump(resolved_packages, f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate package download information."
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help=(
|
||||
"Path to the output YAML file. Example: python generate_downloads.py "
|
||||
"downloads.yml"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.output_file)
|
||||
@@ -0,0 +1,17 @@
|
||||
#A list of third-party packages to surface on the third-party page.
|
||||
packages:
|
||||
- name: "trustcall"
|
||||
repo: "hinthornw/trustcall"
|
||||
description: "Tenacious tool calling built on LangGraph"
|
||||
- name: "breeze-agent"
|
||||
repo: "andrestorres123/breeze-agent"
|
||||
description: "A streamlined research system built inspired on STORM and built on LangGraph"
|
||||
- name: "langgraph-supervisor"
|
||||
repo: "langchain-ai/langgraph-supervisor"
|
||||
description: "Build supervisor multi-agent systems with LangGraph"
|
||||
- name: "langmem"
|
||||
repo: "langchain-ai/langmem"
|
||||
description: "Build agents that learn and adapt from interactions over time."
|
||||
- name: "langchain-mcp-adapters"
|
||||
repo: "langchain-ai/langchain-mcp-adapters"
|
||||
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
|
||||
+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
File diff suppressed because one or more lines are too long
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 +1 @@
|
||||
eNqNVVtMHFUYXnoleKnGSL01nG4qJHVnd2Z2C+z6oLBQ2yIXYZtKUcnZmcPOwMyc6cxZ2uVmhPqk0M5TE/WpLLtmxQKxalP1SUiqVmyaNkqjyIsxhgRjfCs2eGbZ5VJu3aeZ8//f/33//51/tjfZjgxTxlrOsKwRZECB0BfT6k0a6FQUmeRsQkVEwmK8rrYhNBg15KnnJUJ0M+DxQF12Q41IBtZlwS1g1dPOeVRkmjCCzHgYi7E7ObmdThWeaSa4DWmmMwA4lve5gDObRU+aOp0GVhB9ckZNZDhpVMBUikbsI0neD44WqSCMw85uF1jOhaYpm4TS3wc4IoNyHF4EBRUYFZEb1MgCAgQDFSECYji6HxzBp4EANXAUSEjR7TMaF2HspdUk6wg6LUFSZAI1BjSoIpr/pt0OFpFiR4U0I+NlDjEm1jREGAUSOkVnd1JCUKSjPheXsEmssTXDG4GCgHTCIE3AoqxFrE8jHbLuAiJqsWukBLte2h0r1YaQzkBFbkeJRZQ1CnVdkQVoxz2tlHs4o5khMR2tDafs1hhqgUasL8uyOjx1Meq1Bli31+fmR88wdMKyplCzaB9UUkJPx79aGdCh0EbrMJl7ZCUWwZdW5mDTGqqGQm3DqpLQECRrCBpqse+zledGVCOyiqxksG4tXSa4TOd1c5zbP7aqsBnTBGuoBSomGlsa8hIkxbO8l2GLGZa7lJ2SgrQIkaxBvtj3sYFMne4A6kvQkiRq9sapI+j6tWTm1l6srcq6Oe3YG6+g7ljfnECiC/A8OEZvFa1/CPBcgPUHOA68Uh0aDmZoQuuaMRYyoGa2UEMqs+YnBSmqtSExFVzX9innclsG5VdkVSZMZmWpWfarFfexLDtVuGmmgVQ6NZsx7vX7/VvUpZNBxLps90eHx/B8KNMle3J9HlnTo/QKprc/oyphq6K6Dm6Zv6wtiyl8AMwGCrmTU0XroXGUrJE4VJpme2Hr/GWJGUzRg2A2lgjWg983vkWiA5tkrhzcYjbYNHtDPamM84wsWl/T52aWa2wMHe84Vl7devzVGkmr9tbprWYJGmyXoZXi3ByIYBxR0EjwMBOEgoSYhvQKWcmKxpqy6qPB4deZehzG9C6FIL1zGtZQogEZdDWtlKDgqEg/dgZKUHh9WaN12c8W86XFLB+GXlQi8j6m8kT9aHaZlpYlbn8p038479CVNejRRE5OwXu5jvRvu3iurGr85YffXfj+/L+hOyc95huP7ZmdftLiJ8d/u6TWFz09ytz7++bEzomGmfnpzvM7r+Y+usv/Bfr87dLrt7u6uj6Y++mvy1Xeu9/27Hgo0H9lqG684HHXgbmSmcrAa7V5+bsGfunruzH/3NgVZi4POm9ddM8mDs4+49pRWFmve36Ihv5p+6R5oC8y+d35ghsk3P5fS/9e76niP12tgZvj3O73n63qvTf/yPBIfkVMeaIDbtt9ZOz2R7k9Z2cih/NyB4zfm65d8Py6bV9/Y9Mfew7k/Hwrv3dPkRIsH9nXMF9S91Z3ecHCi5N3Pwz9eLVnu8OxsLDdIZY/lWrOcTj+B1mCCwM=
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVGtMHFUUXoQ0FavRGAt/bCcThCrMMjMsWxarlAIlpVIorKW8bG5nLuzAvJjH8rKmYKkxlbRDUxoTCElZdulKYRGMTaiExpKYtFFrDbAxaeIfpRok0USNreDdZReovDrJJDP3nO+c73znnNvqcUJF5SQxYoATNagARkM/qtHqUWCdDlXtjFuAmkNiXYUFxfZeXeH8rzg0TVbTk5OBzJmBqDkUSeYYMyMJyU4qWYCqCqqh6jopsY3++mZcAA0nNKkWiiqejlEkbUnC8LATOilvxhWJh+gL11Wo4MjKSIiJqAWO6h1AS1AxoRETgQAz8FOVAbTEQj5gZXigs5BIIVIJVRJFqBE80BBn/JTHAQGLCjvvckiqZgyvoToEGAbKGgFFRmI5sdq4Vt3EyUkYC6sCMbxMIF5QC8NbC6FMAJ5zQvcSyvABWeY5BgTsyTUo90CIM6E1ynCt2RsojUAVi5rxeWaYR3JhI1JWxEhzisVM+xoIVQOcyCNtUB2IklsO2sdWG2TA1KI4RKhrhnsJPLjaR1KNvnzAFBQ/FhIojMPoA4pgtYysPld0UeMEaHiyCtemCxlX0qWYKcpsG34ssNooMkZfFeBVOLws8jLES5N0CkFaCZIaDKvEQ7Facxi9FGXrV6Aqo4mD77tRSE1XW12oI/DOV57QkFwpOBzu5n1TjCsbdcf4ogSySRhNY3lAxFD8VIym0klbOkVjufn2gaxQGvu6zRi2K0BUq1BDcsLN9zAOXayFrDdr3bb78ZWyFJSf5wROI0ILgpoV+DVcFpIk/fGbeipQQKoFMrpSbDbbFnGRMlAzRgP1IfEImraHqqTK1s/DibKORjC4bCFW7gArxOu1Lf1XuIUx8U+A2YAhXeZPWA8t6doain1pwWyJW/uvUAxhEp4EszFFbD34/+RbShS3iedq4Za8sU29N+TjDXWe4FjjBvo+QVJHGw5AzuI8mi2WHM+rLSnJrSm0C1KvkwOGlzJTWLUkVfNwKOsgkQUYBySKgytkeLJLj2TmH8oaOE4USSclNEt2gGZOlEToLoYKWk3Dy/CSzqLLToFuBC/KLDVGbaSVTrOSaawtjdzL0hYip6TIF16m5WVxBW7K4PXeglZWQUeTEW/vPrfdFHwi0bu4yF7IPxyZuePsYt6dyduDo+VvXI95tb/w23Otu7I766z6B1/H6x1XO79bNI+V9TdE75vPPeCMLRsYnv6riu7O2Nc01nP3J+fDR1pGzPi/12+Mf/mUMOa+meP9rds3EVdnOpLab7Mm+S534lOzvbeJSyO/X7GD+MRPDulvdttf2jnb1j7je8//cl77+Nm/02oqTv/RWflRuaOt4NljST2+S12nKwsu2Dp+MU3sOTj7IErsmp94RtpzdQTb0XMzffbp7yvoNojPRRe2TEq1P1ujWi53/NNzpvkiz8zd3TZXgW93bvumg3n909hjUfHPz3SM+u81dBVVvPPjwmf7U0v3ftgVzbTfEhgbtRCdt/vjmRenupgk8vziQH1sQ/c1qvSFxKl73v3z7+a2F1h2zTxKfeu5Jnx6YTruB8o29OdDKahdpOnXnbfuP4gwmf4DBVoOEg==
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVF1sFFUUbmlTEYPBYCTGGIahpQY6uzOz28q2prpua2Vr6d+G/gji7czt7tCZucPMncqKgJZiNI3iGAzExBe7P82mlFZRVGqEGgTUpn0huCiYJhp5MvEBmz7VO9vdttilZZ7u3HO+c77znXNuT7wb6oaE1NxBScVQBwImP4bVE9fhfhMauDemQBxCYqShvjnQb+pSsiiEsWaUO51AkxxAxSEdaZLgEJDi7OacCjQMEIRGpAOJ4aRxkFbAgb0YdUHVoMspjuXdJRSdcSI3Lx+kdSRDcqJNA+o0sQqIMFGxfRWSNlE7ihWqA3XQh/bYSCRC2bYIMjBFyLiYUsZAqgoxIwNM+NKH4iEIRFLU8UgIGdgaWULzDBAEqGEGqgISJTVonQ6+LmkllAg77RgJwY6X0sFKdEGoMUCWumFsDmUNA02TJQHYduc+knswzZfBYQ0uNSfsshhSrYqtc94MD2dDmKiqUqzD5XbwwwcYAwNJlYkupA5CKaal7OcXGzQgdJE4TLpjVmwOPLTYBxlWtA4I9c13hQS6ELKiQFfK3J8vvtdNFUsKtOK+hqXp0saFdC4Hxzk8I3cFNsKqYEU7gWzAkXmR5yEJnuVdDFvGsNxQRiUZqkEcsvo5rnRAh4ZGpg0ejZGQ2DR6IqQj8Ocr8fSAfFpfm+nmrZwNkSrSHevbFiiWUDxP+YFKkfilFM+Vs55yjqVq6gKDvnSaQNZmjAR0oBqdpCHVmebHhZCpdkEx4cva9iS9UJZO8suSImEmvRykWfavFXGzLJvcsqynDhWimp0x4vJ4PCvEJcpAbJ216yPiMTwfmKuS9bRnzyOpmklGMLVoaVYxmxXhtXVF/wVuGcyW+8BkZ8ix7cnibGhk4iUUo9tT2bat7L9AMY0pvh/MvSlS2eD/k28uUeEynouFm/OmlvW+J59EuvOMJFqj5LyX5ZqQx2sohrC/1CeJ1Ri5db/ZsLO/WwJWgnNwVBChoAzP+F5gfEAIQaY5tUJWvKptp7duh2+wlWlCHYjMUgCQmVORCmPNUCeraSUEGZkieex0GCPwJm+bddbDlvHbSzuhS+Q6nhZ5N1Pd0jScWab5ZYnYL2XqaX+LrKxOri7lrtvYtzon9eWJx7213z+37tjsj92OV8o+lO98tsHbeKNvYPUaGtx88tfo1Zc+/iT69uSs48HHHq3P+3361tHHNxUMT8bq9uw6F579qqL35PWnJl6rv6nPJHdN5bqvTE6dGDvxW4HXNzGev9vPvbs5dy3zgHyhB+1+uLWk7yeae8QP/EbjRz+8WTBR+OKqf5Vn3ltzuO7OTGtrtTm+8aEvKruujY/iob/7a4oG26sa8/461v6PN1yz7/SXv+D1zWMX17Zob1z/oPf8NzUnT11Y1TRQNJ3/LD1VuTmcX/u15P5jpqji+Yijlhn1+V/d1n+x4rtLN6ZvF/LXjuT9eXX69tiRy22H1wvvX0ak9NnZvJzkE+9Uyrk5Of8BO5PlsA==
|
||||
@@ -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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
|
||||
@@ -0,0 +1 @@
|
||||
eNqNVgtsHFcVdRoSNaohFrRFKohMNoBL5RnP7Gd2165p7PUn/mzsjR07/pTl7Zs3u2PPz/NmvF5bJqrbIuVTqUObiICCqGPvFtdxUhKS1E5ctTQiQJGIoAiH5oMEpKFCCokgH6qGN+vdZk0i1JX28+ade++59553345nhpCBJU1dMSOpJjIANMkC2+MZAw1aCJvPpRVkJjRhsq21veOAZUiLTyRMU8cV5eVAlxhNRyqQGKgp5UNcOUwAs5z81mWUdTMZ04TUuQd+OOpSEMYgjrCrguoddUGNxFJNsnB1EZNSTJkJRCURIF8GJakUBiolGkCFEobaU64yymVoMnLwFkaGa6yMKnSiWrJcAAEYS9gEZIs8NDVNjkIgy7nQZkrPgkRLzabqYCTBeeKAomoChXmuoRsHmi3WbEuMhJuFEZTc5MA+Makg0VWgZP3EkRnNEXcwwIhbCmHlRHON9rlkDQLHpo+s+1ztJK36fFp9rjHX2NjT/5OLq9EpB7ZUNbW+MG8nj2X5RD8N67GniYmiCUjOctVN2qvRiqRKjitsGggoZEMEMkY53/+nTJ8mfwFhaEh6DuYKEXaUqVEEtazFoqEpFKCwjqAkSpDKl4lxfOjAIM6JGHE2km4QkRmmhJaWeWh2kadJUpHUOCmnUzEiXMlATnV676KdQuTRWqwfQZOgSfUzCQQEEupCUclkQsOmPbtc1ocAhIjUDalQE0gM+2B8RNLLKAGJMjDRNOmcirJlsacHENJpIEtDKL1kZR8Gui5LSxTK+7GmzuQ6TTtc7t2eduRNk4OimvbRVkKiurG8LUXOn0pxjC/I+A8P00TakiqT80TLgPBJ69n9+cINHcAB4oTOnW07vWQ8W4jRsD0VBrC1fZlLYMCEPQUMhfceKXxuWKopKcjOhNruDZfbvBvOw3Ac4399mWOcUqE9lZXa8WXGyDRSNNSID/sVNg01bUBC9uK1aBSK0ZhSxYU7A10+tj3CiO5wKi4ODIuJaj8PtjYJzfJmFPGI8db2wToc3xyhOb/H7wv4g16O5hiW4RiOhnEVpLguhek0re4hlBr0DXQkgej1dcTr3KCV2QJDrYEaJhVqCDb5Gjw1zbhnG5I7e7byAl8f5YaZmlh9XUTqb2wZdFvQr6HBCCNUV1KEnTUkCVWMz1T4hk1SKNVVW60EIon6pMobrKYEN/cMpQY7msODI+ymlmhUKKTHe700m2PIs94A67xm89qQkRo3E/YBnve/aiCsk0mKnk2TkpkWHp8kOkTvnsnkJupEa/NdCT86WUs0aZ/qQkIZ5XZTTWTeuFm3j3J7KrhgBeumGsIdM6FcmI77SvD1DjKgsEhkWJeXfAYmLHUACdOh+4r9lCN20kmHPhlXNBrWNYzoHCt7Zhu9ZekuoRtrjyydLFoz4kCVRrJh7VNZ1SdHhpMCtAQhMZRU2OCI1yPFkAXFozkTMgecMIQQrWD7gM/DzeZ28rqbJrmyNMfSLDc3TBukFLKkSKSe2c/chYbtSR8p9ol7AaY2gMjVl/Fmu8EuFCIMpBDBOrHvuvEGg8GT9wflXXkIJMgH5pajMCpkw7kVfOJeQM7FBItnhvNoWhLsxa+SRVQIAE/AD3m/3+MTeShwAT/PBVkQi3lYkfUE33AmIiRenGbqmmHSGEFye5spe7FMAcPOjKnycD4PTzKtJFculC0BtVuxWs3JAVdSuoFkDQiHQvV0CMAEotuz+rMztd2bq8ONoWPb6EIh0a360j+HjKphVRLFdDsySGPsaShrlkCGpYHSxNeW6m77aJDlPV6vJxYApDiiGKRryBjKe/tEdpPOpM0AmXAfgvaRhKfKVUGsXJWUAqoCPGlT9v/FM+ml6X96xcy6XQ8WZV8ryfvOnd1bqtU/sSUnP3yk90cH3za+fOafB4OT9Wd7Vz5cF3nQ+8b3+KlN4/NNj33m49H1vzwmH101chaIce+R948Xb7S39ral34vNfXBt7e7KW79feP+Pl8/PHv/39apvfufknc7tnz33Uk/xzsPbazaUvjf6hbkP2h6/uYNf87NLl3pe9v/1H+mTvV/ccHbVLvVFX+Sadaby2suH9r5mLZRdj+GO488U//Tc2zeKiy4pH30ueOp0tP+hX59ec+XFbv+rxy6vL9oz/qWSqX076kYju/Z533roNncb7ro63tPTsvErC4m1axtHQ/IDJStvXHhzYf7h0pnRP4RWXKxMvrP2spfxcs9Xrbvl+dZLpeprz/3g2XUfv/v5iccWQ6uvQvl89JEI9zzT9LU/l95+S29vGeq/8K+WR3/xwuNrJs7vvri/tkvdU3f2LzdjA79K/G5i/57rOy3tG30li3WXn/r5R78Z/Xt44da6/+xjVx/48cFYTfPf5q4+uVc/8c72F/Ynn1zdufvKvuLOmu9/N3l424cXwjdeubln1fz01y9+e+eO9IafwK2zv907sn2ef6JSeHOF05KVRVc2Xqxxk/78F5RiDCU=
|
||||
@@ -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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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
-1
@@ -1 +0,0 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNrtnU9v3MYVwGv05kOPPbOLAGkNccX9L62gw1qSLVmWZXslR0piLGbJx+V4SQ49HK60Mnyo23vBol+gsSM1guMkcNAmadNbgR76BexDvkG/Qx9Xq0iCA80iK6kG8nSQVktyyPnNmzdv3rx5fLLXAxlzEV56zkMFktkK/4nTJ3sSHiYQq9/vBqA84Ty7vdpce5pI/uqKp1QU1ycnWcTzIoKQ8bwtgsleYdL2mJrEz5EPg2KetYXTf33pF49yAcQx60CcqxsfPMpJ4QN+yiUxyNyEkbMF3jtU2VdzLDT6IjEU+L4RgLGFRRpdHjqGcA2uIIgNbgS84ynDzb7loaE8MFzh+2KLhx0j8pkNdePdNjhSiODdvHGTx8rAUnxg+KFkxIntDYuKIWKSqew6LCUw2n2DGViBgOWNxqB0208cwC/bkoNrOBDbkkdZ5bLnATYsKZ97fB/rEQgH/KwWnUiZZWEGPORZ/WIlgQV4QMkEHu95wByk/odnnohV+uIkx8+YbQNeDqEtHHyw9NPODo8m8NauzxTsI6sQBq2U7ncBIpP5vAe7B1eln7Mo8rnNsuOTD2IRPh+yNVU/gjcP72dNYGLLhCr9uhH3Q3sVn6SxNHm7j60eGoV8pZwvfb5txorx0MdWNH2GD7UbDY7//fiBiNldLMkcSlS6e3Dxi+PniDj9eIXZq80TRTJpe+nHTAbV8svj38skVDyAdG/u9pu3Gx48ul0pXyjkp784UXBWo/TTwZ/64DcXfztRCCjZN22BZaV/tl4cwvIh7CgvfVqsWX+REEcoy/C7XbxMJfGTZ9gw8J9/7w1l+qPV5cMW/e5nv3w2j42UfrvmJRNGyTJuoDgXrWLFKEzVK1bdsozrK2vP54a3Wcva5BXK+raahF72zYGkzBjYk2QMajZRrjn1xZpkYexiQy0cCsWe7SVhF5z9uR8Uh28zccDqZfXBzmbCdiRiMIePmT7fMO8edG9zaf7lgeyZQnZYyHcGspF+krUyPgQPvxwejqTIisSbm0GcPp2yrBfDI4cNsI8VtcyCZVqFb7ZN7Fbg84AjzMHvoT7B9i9Z+PPVm2co0QVUPZ8UKtbBzz+PnyMhwKfJbn9UUnEaf/7xw2d9X1o5O2m6VPvm5HlI96ikp8Ug/urN48MyPrLi59uHJ5vcSV+9g/+0gFn2tAsFlxWhWrGdsmW7UxYwZtUK03a19jU2JrexlKz1IiGxdcFGDar66auJgG1n3W62VKiUqljXmUNd00za8yKrRDxjRBJ8wZzPbNe0UdWAeSCB6d785q3GytLcfhMfck6ILoc/vr7081bLdlvtYHZttXFnaWO5y27aJZevb7WW8+sbd/qbc+5WaaW0+F5oNRbuRe31yOuahVppqljNmsQs5K18IV8wweq9X72zsHh1feX20sY1pZbCUsIKttVbDK6WCg+u32t0nZ5bndsJa1Vxrb+6FXduvn9vsSmChirI9WWxcHdnZVMK5t29PrW5HFar3lYDa8OUNzs5Y6AwcuQ7O+wjJvYRM+sh0/XiYQ+ZMZwBg9n8SeU4Yyzi4LMa+v0Zo5nBBPzLAmiiEp69JUJ49SdkkPS4M+vOte4E6xs3Fkq3mgXnejPpNb319fV2ZVmVWwv3FjaDKSj2ytu3y9YxCOVyxbSGHKpWeWogh0eP/iOf6q8b5vEub65GB6PsXijikLvubhMk9qJ03/ZF4qCel7A7d82829hMv5y2WIlVikW7Xa1VWcUyF967u8d8FKaenb70SrO5erlcys0YAZudqpYtazDo/nY3E76w8/pf/3WYYnXjUY47uXouG6FtHJ/NRm9N9XhYY+vTfH4nqT501uza9g1wvLUIx+ScaD9AtTK8In80pucHigdPsFFRKcAyD+BlXXHicAA8Pv6ZqBXKplUzC1N4FY42PW5DS3Ec+es5HNRY4qvsQD/GcbSFY3oHZISPnt3bjVq1Ijg11q6U7eyensCL0YhAGwIHf9jO1a0JLMRXLFc/NCtyDLUUdpQwK/Z7yyKH/0hwk5jh84WJ7z+eyPmig1qtHR98MZHDm/PYa2HFcGQcnnX/8eXLPx2CR7iaKMU5YnQqo18RoNMBGYtAUqSFxIiRlpHyJBAlHaVs5ooUCNPpmAZzf6KkodQXCTHSMBo4ooiShlLmqCNIOqVEeltrKBEhDaGh05s4nc6p/mH4IXU3DaQCATodUJ4AadTRlSuE6HREV4EsIw0iEiLtcEaANIpozeM04dfOQAiR1g3Zj7jNfL9PpLQOW3LXav0iAmWJKGkoRSIrgSidTkm4hIhUEjmQLobTBAHSLkWGWRQOlkaoSHWT5//cVyKZUhJimsLpQMVJlMXikt9NbwvQLJf00hkYlMRI5weQLKCpCa22jSlGS+QlGSFAEshEolCksRH1mOQiIVHSWtt8hzqcltKvidDphLIt28RINxmhjqZdAtiisY182+NK0cMEgMSIxGhcv78kRBpEXVo/0kL6DQHSGUa0/UjLKIkTimsbyTcyyNJCpvYIjn+HtLeek8+75PvXupE8AEV9jkzucWNJeZYmkASJBIlMyvO3AHwWdkltj7IPgDwBI6zcIiZa4dZxchJJVvcIBqUPEBElTcgNpUnQQyoSIArcojQJ54voFuXZGiFIQtGshLJJUDaJc9fXDSKkM68D5tPOdm00Emv75PunMInxnSOszUMgA0nr/s/eDUQmkn7pNuYOKSbKJ0Hb/2ix7a1IkKAo3naECC7qa3qdLQiRBpEnfJIj2vg/dlwbC2iBjYb+cfuZzyRlI9PO/31hd4kSdTbytJ23BwkkUqCsrfSOLQr+vzAfkhBdEiVS3mMj6vgsjmnrFgVsjStIFI40ajgSdTatRSlFj1bbRkoo3YOQA70LQB99o4RkHRIp2sR1FumSpctsMppoE9cZMAozu0lxSnarJQU4UQkVZz51PNqkND6kEgGiOS9tUjpfRPME6HRA2ftJgKwk2qFEO5Roh9L/PRMQ2DQNoVeTjT/vT2TIVSJJmHSktriiJO7a97hhqTyibYH6RDeSbYEk9whtCyDn7YWtKlFqqZGCcZVHCyYUjjv28qSN58XY6UiYSJhorfsCZruot8kOoBB4CoGnhbe3R4zm6bXuekrkCRglSxDFSYzwXld6Bw69H/hM3ntLkGhOcgZetz4tBpAkURali8p+w3rU3Sj7zdgLSj6jfUoj7i0hTjrNTWpbn9eNkk3RGsm4AUoetylCidxIZ5H2lhBRXBLFJV1IICDYQjLFad5GK5KUSYkcAG9JHDeXkgSJVv9/DCA9k1w8mPAeUflgfvXWwv3Ll/8HjuzTXg==
|
||||
+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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
|
||||
@@ -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"); do
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
|
||||
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
|
||||
# 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"
|
||||
@@ -10,4 +21,4 @@ done
|
||||
|
||||
if [ "$ERROR_FOUND" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -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/) |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 578 KiB |
@@ -0,0 +1,15 @@
|
||||
# Prompt Engineering in LangGraph Studio
|
||||
|
||||
In LangGraph Studio you can iterate on the prompts used within your graph by utilizing the LangSmith Playground. To do so:
|
||||
|
||||
1. Open an existing thread or create a new one.
|
||||
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
|
||||
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
|
||||
|
||||
{width=1200}
|
||||
|
||||
|
||||
|
||||
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
|
||||
|
||||
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
|
||||
@@ -99,7 +99,7 @@ We can stream the results of a stateless run in an almost identical fashion to h
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--url <DEPLOYMENT_URL>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
@@ -144,7 +144,7 @@ In addition to streaming, you can also wait for a stateless result by using the
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/runs/runs/wait \
|
||||
--url <DEPLOYMENT_URL>/runs/wait \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_IDD>,
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
# How to integrate LangGraph into your React application
|
||||
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
|
||||
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
|
||||
|
||||
Key features:
|
||||
|
||||
- Messages streaming: Handle a stream of message chunks to form a complete message
|
||||
- Automatic state management for messages, loading states, and errors
|
||||
- Conversation branching: Create alternate conversation paths from any point in the chat history
|
||||
- UI-agnostic design - bring your own components and styling
|
||||
|
||||
Let's explore how to use `useStream()` in your React application.
|
||||
|
||||
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @langchain/langgraph-sdk @langchain/langchain-core react
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [{ type: "human", content: message }] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button key="submit" type="submit">
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing Your UI
|
||||
|
||||
The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
|
||||
|
||||
- Thread state management
|
||||
- Loading and error states
|
||||
- Message handling and updates
|
||||
- Branching support
|
||||
|
||||
Here are some examples on how to use these features effectively:
|
||||
|
||||
### Loading States
|
||||
|
||||
The `isLoading` property tells you when a stream is active, enabling you to:
|
||||
|
||||
- Show a loading indicator
|
||||
- Disable input fields during processing
|
||||
- Display a cancel button
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
const { isLoading, stop } = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<form>
|
||||
{isLoading && (
|
||||
<button key="stop" type="button" onClick={() => stop()}>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Management
|
||||
|
||||
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
|
||||
|
||||
```tsx
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
|
||||
threadId: threadId,
|
||||
onThreadId: setThreadId,
|
||||
});
|
||||
```
|
||||
|
||||
We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
|
||||
|
||||
### Messages Handling
|
||||
|
||||
To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
|
||||
|
||||
When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
|
||||
|
||||
```tsx
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
export default function HomePage() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Branching Support
|
||||
|
||||
To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
|
||||
|
||||
A branch can be created in following ways:
|
||||
|
||||
1. Edit a previous user message.
|
||||
2. Request a regeneration of a previous assistant message.
|
||||
|
||||
```tsx
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
"use client";
|
||||
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
type StateType,
|
||||
type UpdateType,
|
||||
} from "@langchain/langgraph/web";
|
||||
import { useState } from "react";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
});
|
||||
|
||||
function BranchSwitcher({
|
||||
branch,
|
||||
branchOptions,
|
||||
onSelect,
|
||||
}: {
|
||||
branch: string | undefined;
|
||||
branchOptions: string[] | undefined;
|
||||
onSelect: (branch: string) => void;
|
||||
}) {
|
||||
if (!branchOptions || !branch) return null;
|
||||
const index = branchOptions.indexOf(branch);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const prevBranch = branchOptions[index - 1];
|
||||
if (!prevBranch) return;
|
||||
onSelect(prevBranch);
|
||||
}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span>
|
||||
{index + 1} / {branchOptions.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const nextBranch = branchOptions[index + 1];
|
||||
if (!nextBranch) return;
|
||||
onSelect(nextBranch);
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessage({
|
||||
message,
|
||||
onEdit,
|
||||
}: {
|
||||
message: Message;
|
||||
onEdit: (message: Message) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<button type="button" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const content = new FormData(form).get("content") as string;
|
||||
|
||||
form.reset();
|
||||
onEdit({ type: "human", content });
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
<input name="content" defaultValue={message.content as string} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream<
|
||||
StateType<typeof AgentState.spec>,
|
||||
UpdateType<typeof AgentState.spec>
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => {
|
||||
const meta = thread.getMessagesMetadata(message);
|
||||
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
<div>{message.content as string}</div>
|
||||
|
||||
{message.type === "human" && (
|
||||
<EditMessage
|
||||
message={message}
|
||||
onEdit={(message) =>
|
||||
thread.submit(
|
||||
{ messages: [message] },
|
||||
{ checkpoint: parentCheckpoint }
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.type === "ai" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
thread.submit(undefined, { checkpoint: parentCheckpoint })
|
||||
}
|
||||
>
|
||||
<span>Regenerate</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<BranchSwitcher
|
||||
branch={meta?.branch}
|
||||
branchOptions={meta?.branchOptions}
|
||||
onSelect={(branch) => thread.setBranch(branch)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [message] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button key="submit" type="submit">
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
|
||||
|
||||
- State shape
|
||||
- Update format
|
||||
- Custom events
|
||||
|
||||
```tsx
|
||||
// Define your types
|
||||
type State = {
|
||||
messages: Message[];
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type Update = {
|
||||
messages: Message[] | Message;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type CustomEvent = {
|
||||
type: "progress" | "debug";
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
// Use them with the hook
|
||||
const thread = useStream<State, Update, CustomEvent>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
If you're using LangGraph.js, you can reuse your graph's annotation types:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
type StateType,
|
||||
type UpdateType,
|
||||
} from "@langchain/langgraph/web";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
context: Annotation<string>(),
|
||||
});
|
||||
|
||||
const thread = useStream<
|
||||
StateType<typeof AgentState.spec>,
|
||||
UpdateType<typeof AgentState.spec>
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
The `useStream()` hook provides several callback options to help you respond to different events:
|
||||
|
||||
- `onError`: Called when an error occurs.
|
||||
- `onFinish`: Called when the stream is finished.
|
||||
- `onUpdateEvent`: Called when an update event is received.
|
||||
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
|
||||
- `onMetadataEvent`: Called when a metadata event is received.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
|
||||
@@ -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)"
|
||||
|
||||
@@ -27,12 +27,19 @@ LangGraph Platform provides different security defaults:
|
||||
- Requires valid API key in `x-api-key` header
|
||||
- Can be customized with your auth handler
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth **is supported** for all plans in LangGraph Cloud.
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
- No default authentication
|
||||
- Complete flexibility to implement your security model
|
||||
- You control all aspects of authentication and authorization
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth is supported for **Enterprise** self-hosted plans.
|
||||
Self-hosted lite plans do not support custom auth natively.
|
||||
|
||||
## System Architecture
|
||||
|
||||
A typical authentication setup involves three main components:
|
||||
|
||||
@@ -83,12 +83,12 @@ node at a time or if you want to pause the graph execution at specific nodes.
|
||||
|
||||
### `NodeInterrupt` exception
|
||||
|
||||
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
|
||||
We recommend that you [**use the `interrupt` function instead**][langgraph.types.interrupt] of the `NodeInterrupt` exception if you're trying to implement
|
||||
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
|
||||
|
||||
??? node "`NodeInterrupt` exception"
|
||||
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of _dynamic breakpoints_ is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
|
||||
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
@@ -49,9 +49,9 @@ For more information, please see:
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
|
||||
With a Self-Hosted Lite deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Durable Execution
|
||||
|
||||
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
|
||||
|
||||
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
|
||||
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
To leverage durable execution in LangGraph, you need to:
|
||||
|
||||
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
|
||||
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||
|
||||
## Determinism and Consistent Replay
|
||||
|
||||
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
|
||||
|
||||
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
|
||||
|
||||
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
|
||||
|
||||
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
|
||||
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
|
||||
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
|
||||
|
||||
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
|
||||
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
|
||||
|
||||
## Using tasks in nodes
|
||||
|
||||
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
|
||||
|
||||
=== "Original"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
url: str
|
||||
result: NotRequired[str]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
result = requests.get(state['url']).text[:100] # Side-effect
|
||||
return {
|
||||
"result": result
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"url": "https://www.example.com"}, config)
|
||||
```
|
||||
|
||||
=== "With task"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import task
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
urls: list[str]
|
||||
result: NotRequired[list[str]]
|
||||
|
||||
|
||||
@task
|
||||
def _make_request(url: str):
|
||||
"""Make a request."""
|
||||
# highlight-next-line
|
||||
return requests.get(url).text[:100]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
requests = [_make_request(url) for url in state['urls']]
|
||||
results = [request.result() for request in requests]
|
||||
return {
|
||||
"results": results
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"urls": ["https://www.example.com"]}, config)
|
||||
```
|
||||
|
||||
## Resuming Workflows
|
||||
|
||||
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
## Starting Points for Resuming Workflows
|
||||
|
||||
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||
@@ -0,0 +1,936 @@
|
||||
# Functional API
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
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 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
|
||||
|
||||
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # A placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
```
|
||||
|
||||
??? example "Detailed Explanation"
|
||||
|
||||
This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided.
|
||||
|
||||
When the workflow is resumed, it executes from the very start, but because the result of the `write_essay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed.
|
||||
|
||||
```python
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id
|
||||
}
|
||||
}
|
||||
|
||||
for item in workflow.stream("cat", config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'write_essay': 'An essay about topic: cat'}
|
||||
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
|
||||
```
|
||||
|
||||
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
# Get review from a user (e.g., via a UI)
|
||||
# In this case, we're using a bool, but this can be any json-serializable value.
|
||||
human_review = True
|
||||
|
||||
for item in workflow.stream(Command(resume=human_review), config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}
|
||||
```
|
||||
|
||||
The workflow has been completed and the review has been added to the essay.
|
||||
|
||||
## 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).
|
||||
|
||||
### Definition
|
||||
|
||||
An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator.
|
||||
|
||||
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`][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**.
|
||||
|
||||
=== "Sync"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop.
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
=== "Async"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details.
|
||||
|
||||
|
||||
### Injectable Parameters
|
||||
|
||||
When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include:
|
||||
|
||||
|
||||
| Parameter | Description |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **previous** | Access the the state associated with the previous `checkpoint` for the given thread. See [state management](#state-management). |
|
||||
| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](#long-term-memory). |
|
||||
| **writer** | For streaming custom data, to write custom data to the `custom` stream. Useful for [streaming custom data](#streaming-custom-data). |
|
||||
| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. |
|
||||
|
||||
!!! important
|
||||
|
||||
Declare the parameters with the appropriate name and type annotation.
|
||||
|
||||
??? example "Requesting Injectable Parameters"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory
|
||||
|
||||
@entrypoint(
|
||||
checkpointer=checkpointer, # Specify the checkpointer
|
||||
store=in_memory_store # Specify the store
|
||||
)
|
||||
def my_workflow(
|
||||
some_input: dict, # The input (e.g., passed via `invoke`)
|
||||
*,
|
||||
previous: Any = None, # For short-term memory
|
||||
store: BaseStore, # For long-term memory
|
||||
writer: StreamWriter, # For streaming custom data
|
||||
config: RunnableConfig # For accessing the configuration passed to the entrypoint
|
||||
) -> ...:
|
||||
```
|
||||
|
||||
### Executing
|
||||
|
||||
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"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
my_workflow.invoke(some_input, config) # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
await my_workflow.ainvoke(some_input, config) # Await result asynchronously
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### Resuming
|
||||
|
||||
Resuming an execution after an [interrupt][langgraph.types.interrupt] can be done by passing a **resume** value to the [Command][langgraph.types.Command] primitive.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
**Resuming after an error**
|
||||
|
||||
|
||||
To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config).
|
||||
|
||||
This assumes that the underlying **error** has been resolved and execution can proceed successfully.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(None, config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(None, config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints).
|
||||
|
||||
This allows accessing the state from the previous invocation using the `previous` parameter.
|
||||
|
||||
By default, the `previous` parameter is the return value of the previous invocation.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> int:
|
||||
previous = previous or 0
|
||||
return number + previous
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(1, config) # 1 (previous was None)
|
||||
my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation)
|
||||
```
|
||||
|
||||
#### `entrypoint.final`
|
||||
|
||||
[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**.
|
||||
|
||||
The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
|
||||
previous = previous or 0
|
||||
# This will return the previous value to the caller, saving
|
||||
# 2 * number to the checkpoint, which will be used in the next invocation
|
||||
# for the `previous` parameter.
|
||||
return entrypoint.final(value=previous, save=2 * number)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(3, config) # 0 (previous was None)
|
||||
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
|
||||
```
|
||||
|
||||
## Task
|
||||
|
||||
A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics:
|
||||
|
||||
* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking.
|
||||
* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details).
|
||||
|
||||
### Definition
|
||||
|
||||
Tasks are defined using the `@task` decorator, which wraps a regular Python function.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
@task()
|
||||
def slow_computation(input_value):
|
||||
# Simulate a long-running operation
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **outputs** of tasks must be JSON-serializable to support checkpointing.
|
||||
|
||||
### Execution
|
||||
|
||||
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes).
|
||||
|
||||
Tasks *cannot* be called directly from the main application code.
|
||||
|
||||
When you call a **task**, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later.
|
||||
|
||||
To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`).
|
||||
|
||||
|
||||
=== "Synchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: int) -> int:
|
||||
future = slow_computation(some_input)
|
||||
return future.result() # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Asynchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: int) -> int:
|
||||
return await slow_computation(some_input) # Await result asynchronously
|
||||
```
|
||||
|
||||
## When to use a task
|
||||
|
||||
**Tasks** are useful in the following scenarios:
|
||||
|
||||
- **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow.
|
||||
- **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details.
|
||||
- **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs).
|
||||
- **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/).
|
||||
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
|
||||
|
||||
## Serialization
|
||||
|
||||
There are two key aspects to serialization in LangGraph:
|
||||
|
||||
1. `@entrypoint` inputs and outputs must be JSON-serializable.
|
||||
2. `@task` outputs must be JSON-serializable.
|
||||
|
||||
These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives
|
||||
like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable.
|
||||
|
||||
Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution.
|
||||
|
||||
Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer.
|
||||
|
||||
## Determinism
|
||||
|
||||
To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic.
|
||||
|
||||
LangGraph achieves this behavior by persisting **task** and [**subgraph**](./low_level.md#subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same
|
||||
|
||||
While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them.
|
||||
|
||||
## Idempotency
|
||||
|
||||
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
|
||||
|
||||
### Handling side effects
|
||||
|
||||
Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to ensure they are not executed multiple times when resuming a workflow.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, a side effect (writing to a file) is directly included in the workflow, so it will be executed a second time when resuming the workflow.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# This code will be executed a second time when resuming the workflow.
|
||||
# Which is likely not what you want.
|
||||
# highlight-next-line
|
||||
with open("output.txt", "w") as f:
|
||||
# highlight-next-line
|
||||
f.write("Side effect executed")
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def write_to_file():
|
||||
with open("output.txt", "w") as f:
|
||||
f.write("Side effect executed")
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# The side effect is now encapsulated in a task.
|
||||
write_to_file().result()
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
### Non-deterministic control flow
|
||||
|
||||
Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned.
|
||||
|
||||
* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ...
|
||||
* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ...
|
||||
|
||||
This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list
|
||||
of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value.
|
||||
This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts.
|
||||
|
||||
If order of execution is not maintained when resuming, one `interrupt` call may be matched with the wrong `resume` value, leading to incorrect results.
|
||||
|
||||
Please read the section on [determinism](#determinism) for more details.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = time.time()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input.
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def get_time() -> float:
|
||||
return time.time()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = get_time().result()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
Below are a few simple patterns that show examples of **how to** use the **Functional API**.
|
||||
|
||||
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = inputs["value"]
|
||||
another_value = inputs["another_value"]
|
||||
...
|
||||
|
||||
my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
```
|
||||
|
||||
### Parallel execution
|
||||
|
||||
Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs).
|
||||
|
||||
```python
|
||||
@task
|
||||
def add_one(number: int) -> int:
|
||||
return number + 1
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(numbers: list[int]) -> list[str]:
|
||||
futures = [add_one(i) for i in numbers]
|
||||
return [f.result() for f in futures]
|
||||
```
|
||||
|
||||
### Calling subgraphs
|
||||
|
||||
The **Functional API** and the [**Graph API**](./low_level.md) can be used together in the same application as they share the same underlying runtime.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
builder = StateGraph()
|
||||
...
|
||||
some_graph = builder.compile()
|
||||
|
||||
@entrypoint()
|
||||
def some_workflow(some_input: dict) -> int:
|
||||
# Call a graph defined using the graph API
|
||||
result_1 = some_graph.invoke(...)
|
||||
# Call another graph defined using the graph API
|
||||
result_2 = another_graph.invoke(...)
|
||||
return {
|
||||
"result_1": result_1,
|
||||
"result_2": result_2
|
||||
}
|
||||
```
|
||||
|
||||
### Calling other entrypoints
|
||||
|
||||
You can call other **entrypoints** from within an **entrypoint** or a **task**.
|
||||
|
||||
```python
|
||||
@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
|
||||
def some_other_workflow(inputs: dict) -> int:
|
||||
return inputs["value"]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = some_other_workflow.invoke({"value": 1})
|
||||
return value
|
||||
```
|
||||
|
||||
### Streaming custom data
|
||||
|
||||
You can stream custom data from an **entrypoint** by using the `StreamWriter` type. This allows you to write custom data to the `custom` stream.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
@task
|
||||
def add_one(x):
|
||||
return x + 1
|
||||
|
||||
@task
|
||||
def add_two(x):
|
||||
return x + 2
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter) -> int:
|
||||
"""A simple workflow that adds one and two to a number."""
|
||||
writer("hello") # Write some data to the `custom` stream
|
||||
add_one(inputs['number']).result() # Will write data to the `updates` stream
|
||||
writer("world") # Write some more data to the `custom` stream
|
||||
add_two(inputs['number']).result() # Will write data to the `updates` stream
|
||||
return 5
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
('updates', {'add_one': 2})
|
||||
('updates', {'add_two': 3})
|
||||
('custom', 'hello')
|
||||
('custom', 'world')
|
||||
('updates', {'main': 5})
|
||||
```
|
||||
|
||||
!!! important
|
||||
|
||||
The `writer` parameter is automatically injected at run time. It will only be injected if the
|
||||
parameter name appears in the function signature with that *exact* name.
|
||||
|
||||
|
||||
### Retry policy
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
attempts = 0
|
||||
|
||||
# Let's configure the RetryPolicy to retry on ValueError.
|
||||
# The default RetryPolicy is optimized for retrying specific network errors.
|
||||
retry_policy = RetryPolicy(retry_on=ValueError)
|
||||
|
||||
@task(retry=retry_policy)
|
||||
def get_info():
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError('Failure')
|
||||
return "OK"
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer):
|
||||
return get_info().result()
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'OK'
|
||||
```
|
||||
|
||||
### Resuming after an error
|
||||
|
||||
```python
|
||||
import time
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
# This variable is just used for demonstration purposes to simulate a network failure.
|
||||
# It's not something you will have in your actual code.
|
||||
attempts = 0
|
||||
|
||||
@task()
|
||||
def get_info():
|
||||
"""
|
||||
Simulates a task that fails once before succeeding.
|
||||
Raises an exception on the first attempt, then returns "OK" on subsequent tries.
|
||||
"""
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError("Failure") # Simulate a failure on the first attempt
|
||||
return "OK"
|
||||
|
||||
# Initialize an in-memory checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@task
|
||||
def slow_task():
|
||||
"""
|
||||
Simulates a slow-running task by introducing a 1-second delay.
|
||||
"""
|
||||
time.sleep(1)
|
||||
return "Ran slow task."
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter):
|
||||
"""
|
||||
Main workflow function that runs the slow_task and get_info tasks sequentially.
|
||||
|
||||
Parameters:
|
||||
- inputs: Dictionary containing workflow input values.
|
||||
- writer: StreamWriter for streaming custom data.
|
||||
|
||||
The workflow first executes `slow_task` and then attempts to execute `get_info`,
|
||||
which will fail on the first invocation.
|
||||
"""
|
||||
slow_task_result = slow_task().result() # Blocking call to slow_task
|
||||
get_info().result() # Exception will be raised here on the first attempt
|
||||
return slow_task_result
|
||||
|
||||
# Workflow execution configuration with a unique thread identifier
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1" # Unique identifier to track workflow execution
|
||||
}
|
||||
}
|
||||
|
||||
# This invocation will take ~1 second due to the slow_task execution
|
||||
try:
|
||||
# First invocation will raise an exception due to the `get_info` task failing
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
except ValueError:
|
||||
pass # Handle the failure gracefully
|
||||
```
|
||||
|
||||
When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint.
|
||||
|
||||
```python
|
||||
main.invoke(None, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'Ran slow task.'
|
||||
```
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
The functional API supports [human-in-the-loop](human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive.
|
||||
|
||||
Please see the following examples for more details:
|
||||
|
||||
* [How to wait for user input (Functional API)](../how-tos/wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API.
|
||||
* [How to review tool calls (Functional API)](../how-tos/review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API.
|
||||
|
||||
### Short-term memory
|
||||
|
||||
[State management](#state-management) using the **previous** parameter and optionally using the `entrypoint.final` primitive can be used to implement [short term memory](memory.md).
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add thread-level persistence (functional API)](../how-tos/persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Long-term memory
|
||||
|
||||
[long-term memory](memory.md#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information
|
||||
about a given user in one conversation and using it in another.
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add cross-thread persistence (functional API)](../how-tos/cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Workflows
|
||||
|
||||
* [Workflows and agent](../tutorials/workflows/index.md) guide for more examples of how to build workflows using the Functional API.
|
||||
|
||||
### Agents
|
||||
|
||||
* [How to create a React agent from scratch (Functional API)](../how-tos/react-agent-from-scratch-functional.ipynb): Shows how to create a simple React agent from scratch using the functional API.
|
||||
* [How to build a multi-agent network](../how-tos/multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API.
|
||||
* [How to add multi-turn conversation in a multi-agent application (functional API)](../how-tos/multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents.
|
||||
|
||||
@@ -19,7 +19,7 @@ LangGraph has a [persistence layer](https://langchain-ai.github.io/langgraph/con
|
||||
|
||||
### Streaming
|
||||
|
||||
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/stream-updates.ipynb)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
|
||||
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/streaming.ipynb#updates)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
|
||||
|
||||
### Debugging and Deployment
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -7,7 +7,7 @@ description: Conceptual Guide for LangGraph
|
||||
|
||||
This guide provides explanations of the key concepts behind the LangGraph framework and AI applications more broadly.
|
||||
|
||||
We recommend that you go through at least the [Quick Start](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
|
||||
We recommend that you go through at least the [Quickstart](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
|
||||
|
||||
The conceptual guide does not cover step-by-step instructions or specific implementation examples — those are found in the [Tutorials](../tutorials/index.md) and [How-to guides](../how-tos/index.md). For detailed reference material, please see the [API reference](../reference/index.md).
|
||||
|
||||
@@ -26,8 +26,10 @@ The conceptual guide does not cover step-by-step instructions or specific implem
|
||||
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
|
||||
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
|
||||
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
|
||||
- [Durable Execution](durable_execution.md): LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
|
||||
- [FAQ](faq.md): Frequently asked questions about LangGraph.
|
||||
|
||||
## LangGraph Platform
|
||||
@@ -36,7 +38,6 @@ LangGraph Platform is a commercial solution for deploying agentic applications i
|
||||
|
||||
The LangGraph Platform offers a few different deployment options described in the [deployment options guide](./deployment_options.md).
|
||||
|
||||
|
||||
!!! tip
|
||||
|
||||
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
|
||||
@@ -45,6 +46,7 @@ The LangGraph Platform offers a few different deployment options described in th
|
||||
### High Level
|
||||
|
||||
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
|
||||
- [Platform Architecture](./platform_architecture.md): A high-level overview of the architecture of the LangGraph Platform.
|
||||
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
|
||||
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
|
||||
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
|
||||
@@ -53,7 +55,7 @@ The LangGraph Platform offers a few different deployment options described in th
|
||||
|
||||
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
|
||||
|
||||
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
|
||||
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
|
||||
- [LangGraph Studio](./langgraph_studio.md): LangGraph Studio is a specialized IDE that can connect to a LangGraph Server to enable visualization, interaction, and debugging of the application locally.
|
||||
- [LangGraph CLI](./langgraph_cli.md): LangGraph CLI is a command-line interface that helps to interact with a local LangGraph
|
||||
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
|
||||
@@ -70,8 +72,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
|
||||
### Deployment Options
|
||||
|
||||
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Cloud SaaS](./langgraph_cloud.md): Hosted as part of LangSmith.
|
||||
- [Bring Your Own Cloud](./bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
|
||||
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
|
||||
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
|
||||
|
||||
@@ -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,12 +55,26 @@ 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.
|
||||
|
||||
!!! info "Database creation for `Development` type deployments takes longer than database creation for `Production` type deployments."
|
||||
## 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.
|
||||
|
||||
- An email notification is sent after 7 consecutive days of non-use.
|
||||
- A deployment is deleted after 28 consecutive days of non-use.
|
||||
|
||||
!!! danger "Data Cannot Be Recovered"
|
||||
After a deployment is deleted, the data (i.e. [persistence](#persistence)) from the deployment cannot be recovered.
|
||||
|
||||
## Architecture
|
||||
|
||||
!!! warning "Subject to Change"
|
||||
The Cloud SaaS deployment architecture may change in the future.
|
||||
The Cloud SaaS deployment architecture may change in the future.
|
||||
|
||||
A high-level diagram of a Cloud SaaS deployment.
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ A super-step can be considered a single iteration over the graph nodes. Nodes th
|
||||
|
||||
The `StateGraph` class is the main graph class to use. This is parameterized by a user defined `State` object.
|
||||
|
||||
### MessageGraph
|
||||
|
||||
The `MessageGraph` class is a special type of graph. The `State` of a `MessageGraph` is ONLY a list of messages. This class is rarely used except for chatbots, as most applications require the `State` to be more complex than a list of messages.
|
||||
|
||||
### Compiling your graph
|
||||
|
||||
To build your graph, you first define the [state](#state), you then add [nodes](#nodes) and [edges](#edges), and then you compile it. What exactly is compiling your graph and why is it needed?
|
||||
@@ -217,9 +213,9 @@ builder.add_node("other_node", my_other_node)
|
||||
...
|
||||
```
|
||||
|
||||
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
|
||||
Behind the scenes, functions are converted to [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)s, which add batch and async support to your function, along with native tracing and debugging.
|
||||
|
||||
If you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.
|
||||
If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
|
||||
|
||||
```python
|
||||
builder.add_node(my_node)
|
||||
@@ -228,7 +224,7 @@ builder.add_node(my_node)
|
||||
|
||||
### `START` Node
|
||||
|
||||
The `START` Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
|
||||
The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
|
||||
|
||||
```python
|
||||
from langgraph.graph import START
|
||||
@@ -273,9 +269,9 @@ If you want to **optionally** route to 1 or more edges (or optionally terminate)
|
||||
graph.add_conditional_edges("node_a", routing_function)
|
||||
```
|
||||
|
||||
Similar to nodes, the `routing_function` accept the current `state` of the graph and return a value.
|
||||
Similar to nodes, the `routing_function` accepts the current `state` of the graph and returns a value.
|
||||
|
||||
By default, the return value `routing_function` is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
|
||||
By default, the return value `routing_function` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
|
||||
|
||||
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
|
||||
|
||||
@@ -314,7 +310,7 @@ graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "no
|
||||
|
||||
## `Send`
|
||||
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common of example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
|
||||
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
|
||||
|
||||
@@ -361,7 +357,7 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
|
||||
|
||||
### Navigating to a node in a parent graph
|
||||
|
||||
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
|
||||
If you are using [subgraphs](#subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
@@ -376,6 +372,10 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
|
||||
Setting `graph` to `Command.PARENT` will navigate to the closest parent graph.
|
||||
|
||||
!!! important "State updates with `Command.PARENT`"
|
||||
|
||||
When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](../how-tos/command.ipynb#navigating-to-a-node-in-a-parent-graph).
|
||||
|
||||
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
|
||||
|
||||
### Using inside tools
|
||||
@@ -400,7 +400,7 @@ def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: R
|
||||
!!! important
|
||||
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
|
||||
|
||||
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
|
||||
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node.
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
@@ -494,7 +494,7 @@ Read more about how the `interrupt` is used for **human-in-the-loop** workflows
|
||||
|
||||
## Breakpoints
|
||||
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt) for this purpose.
|
||||
|
||||
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
|
||||
|
||||
@@ -531,7 +531,7 @@ Let's take a look at examples for each.
|
||||
|
||||
### As a compiled graph
|
||||
|
||||
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should use write a function [invoking the subgraph](#as-a-function) instead.
|
||||
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should write a function [invoking the subgraph](#as-a-function) instead.
|
||||
|
||||
!!! Note
|
||||
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
|
||||
|
||||
@@ -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! |
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# LangGraph Platform Architecture
|
||||
|
||||

|
||||
|
||||
## How we use Postgres
|
||||
|
||||
Postgres is the persistence layer for all user and run data in LGP. This stores both checkpoints (see more info [here](./persistence.md)) as well as the server resources (threads, runs, assistants and crons).
|
||||
|
||||
## How we use Redis
|
||||
|
||||
Redis is used in each LGP deployment as a way for server and queue workers to communicate, and to store ephemeral metadata, more details on both below. No user/run data is stored in Redis.
|
||||
|
||||
### Communication
|
||||
|
||||
All runs in LGP are executed by the pool of background workers that are part of each deployment. In order to enable some features for those runs (such as cancellation and output streaming) we need a channel for two-way communication between the server and the worker handling a particular run. We use Redis to organize that communication.
|
||||
|
||||
1. A Redis list is used as a mechanism to wake up a worker as soon as a new run is created. Only a sentinel value is stored in this list, no actual run info. The run information is then retrieved from Postgres by the worker.
|
||||
2. A combination of a Redis string and Redis PubSub channel is used for the server to communicate a run cancellation request to the appropriate worker.
|
||||
3. A Redis PubSub channel is used by the worker to broadcast streaming output from an agent while the run is being handled. Any open `/stream` request in the server will subscribe to that channel and forward any events to the response as they arrive. No events are stored in Redis at any time.
|
||||
|
||||
### Ephemeral metadata
|
||||
|
||||
Runs in an LGP deployment may be retried for specific failures (currently only for transient Postgres errors encountered during the run). In order to limit the number of retries (currently limited to 3 attempts per run) we record the attempt number in a Redis string when is picked up. This contains no run-specific info other than its ID, and expires after a short delay.
|
||||
@@ -11,7 +11,7 @@ There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](.
|
||||
|
||||
### Self-Hosted Lite
|
||||
|
||||
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed).
|
||||
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed per year).
|
||||
|
||||
When using the Self-Hosted Lite version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key.
|
||||
|
||||
@@ -34,7 +34,7 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite or Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
|
||||
|
||||
|
||||
+12
-123
@@ -1,17 +1,23 @@
|
||||
# Streaming
|
||||
|
||||
LangGraph is built with first class support for streaming. There are several different ways to stream back outputs from a graph run
|
||||
Building a responsive app for end-users? Real-time updates are key to keeping users engaged as your app progresses.
|
||||
|
||||
There are three main types of data you’ll want to stream:
|
||||
|
||||
1. Workflow progress (e.g., get state updates after each graph node is executed).
|
||||
2. LLM tokens as they’re generated.
|
||||
3. Custom updates (e.g., "Fetched 10/100 records").
|
||||
|
||||
## Streaming graph outputs (`.stream` and `.astream`)
|
||||
|
||||
`.stream` and `.astream` are sync and async methods for streaming back outputs from a graph run.
|
||||
There are several different modes you can specify when calling these methods (e.g. `graph.stream(..., mode="...")):
|
||||
|
||||
- [`"values"`](../how-tos/stream-values.ipynb): This streams the full value of the state after each step of the graph.
|
||||
- [`"updates"`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
|
||||
- [`"custom"`](../how-tos/streaming-content.ipynb): This streams custom data from inside your graph nodes.
|
||||
- [`"values"`](../how-tos/streaming.ipynb#values): This streams the full value of the state after each step of the graph.
|
||||
- [`"updates"`](../how-tos/streaming.ipynb#updates): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
|
||||
- [`"custom"`](../how-tos/streaming.ipynb#custom): This streams custom data from inside your graph nodes.
|
||||
- [`"messages"`](../how-tos/streaming-tokens.ipynb): This streams LLM tokens and metadata for the graph node where LLM is invoked.
|
||||
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
|
||||
- [`"debug"`](../how-tos/streaming.ipynb#debug): This streams as much information as possible throughout the execution of the graph.
|
||||
|
||||
You can also specify multiple streaming modes at the same time by passing them as a list. When you do this, the streamed outputs will be tuples `(stream_mode, data)`. For example:
|
||||
|
||||
@@ -31,123 +37,6 @@ The below visualization shows the difference between the `values` and `updates`
|
||||

|
||||
|
||||
|
||||
## Streaming LLM tokens and events (`.astream_events`)
|
||||
|
||||
In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
This is a standard method on all [LangChain objects](https://python.langchain.com/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`.
|
||||
|
||||
All events have (among other things) `event`, `name`, and `data` fields. What do these mean?
|
||||
|
||||
- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/docs/concepts/#callback-events).
|
||||
- `name`: This is the name of event.
|
||||
- `data`: This is the data associated with the event.
|
||||
|
||||
What types of things cause events to be emitted?
|
||||
|
||||
* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field
|
||||
* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field
|
||||
* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events
|
||||
|
||||
Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`.
|
||||
|
||||
To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph:
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
|
||||
model = ChatOpenAI(model="gpt-4o-mini")
|
||||
|
||||
|
||||
def call_model(state: MessagesState):
|
||||
response = model.invoke(state['messages'])
|
||||
return {"messages": response}
|
||||
|
||||
workflow = StateGraph(MessagesState)
|
||||
workflow.add_node(call_model)
|
||||
workflow.add_edge(START, "call_model")
|
||||
workflow.add_edge("call_model", END)
|
||||
app = workflow.compile()
|
||||
|
||||
inputs = [{"role": "user", "content": "hi!"}]
|
||||
async for event in app.astream_events({"messages": inputs}, version="v1"):
|
||||
kind = event["event"]
|
||||
print(f"{kind}: {event['name']}")
|
||||
```
|
||||
```shell
|
||||
on_chain_start: LangGraph
|
||||
on_chain_start: __start__
|
||||
on_chain_end: __start__
|
||||
on_chain_start: call_model
|
||||
on_chat_model_start: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_end: ChatOpenAI
|
||||
on_chain_start: ChannelWrite<call_model,messages>
|
||||
on_chain_end: ChannelWrite<call_model,messages>
|
||||
on_chain_stream: call_model
|
||||
on_chain_end: call_model
|
||||
on_chain_stream: LangGraph
|
||||
on_chain_end: LangGraph
|
||||
```
|
||||
|
||||
We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input).
|
||||
We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`),
|
||||
stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there,
|
||||
we write the results back to the channel (`ChannelWrite<call_model,messages>`) and then finish the `call_model` node and then the graph as a whole.
|
||||
|
||||
This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain?
|
||||
Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event
|
||||
since it is needed for streaming tokens from an LLM response.
|
||||
|
||||
These events look like:
|
||||
|
||||
```shell
|
||||
{'event': 'on_chat_model_stream',
|
||||
'name': 'ChatOpenAI',
|
||||
'run_id': '3fdbf494-acce-402e-9b50-4eab46403859',
|
||||
'tags': ['seq:step:1'],
|
||||
'metadata': {'langgraph_step': 1,
|
||||
'langgraph_node': 'call_model',
|
||||
'langgraph_triggers': ['start:call_model'],
|
||||
'langgraph_task_idx': 0,
|
||||
'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c',
|
||||
'checkpoint_ns': 'call_model',
|
||||
'ls_provider': 'openai',
|
||||
'ls_model_name': 'gpt-4o-mini',
|
||||
'ls_model_type': 'chat',
|
||||
'ls_temperature': 0.7},
|
||||
'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')},
|
||||
'parent_ids': []}
|
||||
```
|
||||
We can see that we have the event type and name (which we knew from before).
|
||||
|
||||
We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information
|
||||
which tells us which node this model was invoked inside of.
|
||||
|
||||
Finally, `data` is a really important field. This contains the actual data for this event! Which in this case
|
||||
is an AIMessageChunk. This contains the `content` for the message, as well as an `id`.
|
||||
This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps
|
||||
us track which chunks are part of the same message (so we can show them together in the UI).
|
||||
|
||||
This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a
|
||||
guide for that [here](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
|
||||
!!! warning "ASYNC IN PYTHON<=3.10"
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb).
|
||||
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. LangGraph Platform supports five streaming modes:
|
||||
@@ -155,8 +44,8 @@ Streaming is critical for making LLM applications feel responsive to end users.
|
||||
- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../cloud/how-tos/stream_values.md) for streaming values.
|
||||
- `messages-tuple`: Stream LLM tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. See the [how-to guide](../cloud/how-tos/stream_messages.md) for streaming messages.
|
||||
- `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../cloud/how-tos/stream_updates.md) for streaming updates.
|
||||
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs.
|
||||
- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../cloud/how-tos/stream_debug.md) for streaming debug events.
|
||||
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This mode is only useful for users migrating large LCEL applications to LangGraph. Generally, this mode is not necessary for most applications.
|
||||
|
||||
You can also specify multiple streaming modes at the same time. See the [how-to guide](../cloud/how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"\n",
|
||||
"This guide shows how you can:\n",
|
||||
"\n",
|
||||
"- implement handoffs using `Command`: agent node makes some decision (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
|
||||
"- implement handoffs using `Command`: agent node makes a decision on who to hand off to (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
|
||||
"- implement handoffs using tools: a tool-calling agent has access to tools that can return a handoff via `Command`. The tool-executing node in the agent recognizes `Command` objects returned by the tools and routes accordingly. Handoff tool a general-purpose primitive that is useful in any multi-agent systems that contain tool-calling agents."
|
||||
]
|
||||
},
|
||||
@@ -83,18 +83,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "b4864843-00a1-4c88-9a7c-c34e6c31c548",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_API_KEY: ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
@@ -828,13 +820,13 @@
|
||||
"addition_expert = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" [add, make_handoff_tool(agent_name=\"multiplication_expert\")],\n",
|
||||
" state_modifier=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
|
||||
" prompt=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"multiplication_expert = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" [multiply, make_handoff_tool(agent_name=\"addition_expert\")],\n",
|
||||
" state_modifier=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
|
||||
" prompt=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"builder = StateGraph(MessagesState)\n",
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
|
||||
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
|
||||
|
||||
???+ note "Support by deployment type"
|
||||
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
|
||||
|
||||
## 1. Implement authentication
|
||||
|
||||
Create `auth.py` file, with a basic JWT authentication handler:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "100c0c81-6a9f-4ba1-b1a8-42aae82b7172",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks\n",
|
||||
"\n",
|
||||
"LangGraph is a framework for building agentic and multi-agent applications. LangGraph can be easily integrated with other agent frameworks. \n",
|
||||
"\n",
|
||||
"The primary reasons you might want to integrate LangGraph with other agent frameworks:\n",
|
||||
"\n",
|
||||
"- create [multi-agent systems](../../concepts/multi_agent) where individual agents are built with different frameworks\n",
|
||||
"- leverage LangGraph to add features like [persistence](../../concepts/persistence), [streaming](../../concepts/streaming), [short and long-term memory](../../concepts/memory) and more\n",
|
||||
"\n",
|
||||
"The simplest way to integrate agents from other frameworks is by calling those agents inside a LangGraph [node](../../concepts/low_level/#nodes):\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"import autogen\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"\n",
|
||||
"autogen_agent = autogen.AssistantAgent(name=\"assistant\", ...)\n",
|
||||
"user_proxy = autogen.UserProxyAgent(name=\"user_proxy\", ...)\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_autogen_agent(messages):\n",
|
||||
" response = user_proxy.initiate_chat(\n",
|
||||
" autogen_agent,\n",
|
||||
" message=messages[-1],\n",
|
||||
" ...\n",
|
||||
" )\n",
|
||||
" ...\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint()\n",
|
||||
"def workflow(messages):\n",
|
||||
" response = call_autogen_agent(messages).result()\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow.invoke(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"In this guide we show how to build a LangGraph chatbot that integrates with AutoGen, but you can follow the same approach with other frameworks."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b189ceb2-132b-4c7b-81b4-c7b8b062f833",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "62417d3a-94f9-4a52-9962-12639d714966",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install autogen langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "d46da41d-0a71-4654-aec8-9e6ad8765236",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"OPENAI_API_KEY: ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define AutoGen agent\n",
|
||||
"\n",
|
||||
"Here we define our AutoGen agent. Adapted from official tutorial [here](https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "524de117-ff09-4b26-bfe8-a9f85a46ffd5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import autogen\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
|
||||
"\n",
|
||||
"llm_config = {\n",
|
||||
" \"timeout\": 600,\n",
|
||||
" \"cache_seed\": 42,\n",
|
||||
" \"config_list\": config_list,\n",
|
||||
" \"temperature\": 0,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"autogen_agent = autogen.AssistantAgent(\n",
|
||||
" name=\"assistant\",\n",
|
||||
" llm_config=llm_config,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"user_proxy = autogen.UserProxyAgent(\n",
|
||||
" name=\"user_proxy\",\n",
|
||||
" human_input_mode=\"NEVER\",\n",
|
||||
" max_consecutive_auto_reply=10,\n",
|
||||
" is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
|
||||
" code_execution_config={\n",
|
||||
" \"work_dir\": \"web\",\n",
|
||||
" \"use_docker\": False,\n",
|
||||
" }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
|
||||
" llm_config=llm_config,\n",
|
||||
" system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8aa858e2-4acb-4f75-be20-b9ccbbcb5073",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dcc478f5-4a35-43f8-bf59-9cb71289cd00",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create the workflow\n",
|
||||
"\n",
|
||||
"We will now create a LangGraph chatbot graph that calls AutoGen agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_autogen_agent(messages: list[BaseMessage]):\n",
|
||||
" # convert to openai-style messages\n",
|
||||
" messages = convert_to_openai_messages(messages)\n",
|
||||
" response = user_proxy.initiate_chat(\n",
|
||||
" autogen_agent,\n",
|
||||
" message=messages[-1],\n",
|
||||
" # pass previous message history as context\n",
|
||||
" carryover=messages[:-1],\n",
|
||||
" )\n",
|
||||
" # get the final response from the agent\n",
|
||||
" content = response.chat_history[-1][\"content\"]\n",
|
||||
" return {\"role\": \"assistant\", \"content\": content}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# add short-term memory for storing conversation history\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
"def workflow(messages: list[BaseMessage], previous: list[BaseMessage]):\n",
|
||||
" messages = add_messages(previous or [], messages)\n",
|
||||
" response = call_autogen_agent(messages).result()\n",
|
||||
" return entrypoint.final(value=response, save=add_messages(messages, response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "23d629c3-1d6b-40af-adf6-915e15657566",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Run the graph\n",
|
||||
"\n",
|
||||
"We can now run the graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "a279b667-0f5d-4008-8d43-c806a3f379c4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
"1. Generate Fibonacci numbers starting from 0.\n",
|
||||
"2. Continue generating until the numbers exceed 30.\n",
|
||||
"3. Collect and print the numbers that are between 10 and 30.\n",
|
||||
"\n",
|
||||
"Let's implement this in Python:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"# filename: fibonacci_range.py\n",
|
||||
"\n",
|
||||
"def fibonacci_sequence():\n",
|
||||
" a, b = 0, 1\n",
|
||||
" while a <= 30:\n",
|
||||
" if 10 <= a <= 30:\n",
|
||||
" print(a)\n",
|
||||
" a, b = b, a + b\n",
|
||||
"\n",
|
||||
"fibonacci_sequence()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
"13\n",
|
||||
"21\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
|
||||
"\n",
|
||||
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
|
||||
"\n",
|
||||
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n",
|
||||
"{'workflow': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# pass the thread ID to persist agent outputs for future interactions\n",
|
||||
"# highlight-next-line\n",
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"\n",
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" # highlight-next-line\n",
|
||||
" config,\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c6cd57b4-d4ee-49f6-be12-318613849669",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Since we're leveraging LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) features we can now continue the conversation using the same thread ID -- LangGraph will automatically pass previous history to the AutoGen agent:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "e68811a7-962e-4fe3-9f45-9b99ebbe04e7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
|
||||
"\n",
|
||||
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
|
||||
"\n",
|
||||
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
"21 * 3 = 63\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n",
|
||||
"{'workflow': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Multiply the last number by 3\",\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" # highlight-next-line\n",
|
||||
" config,\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+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\"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"\n",
|
||||
"This tutorial will show how to add a custom system prompt to the [prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent). Please see [this tutorial](../create-react-agent) for how to get started with the prebuilt ReAct agent\n",
|
||||
"\n",
|
||||
"You can add a custom system prompt by passing a string to the `state_modifier` param.\n"
|
||||
"You can add a custom system prompt by passing a string to the `prompt` param.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -144,7 +144,7 @@
|
||||
"\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"\n",
|
||||
"graph = create_react_agent(model, tools=tools, state_modifier=prompt)"
|
||||
"graph = create_react_agent(model, tools=tools, prompt=prompt)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "d2eecb96-cf0e-47ed-8116-88a7eaa4236d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add cross-thread persistence (functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Functional API](../../concepts/functional_api/)\n",
|
||||
" - [Persistence](../../concepts/persistence/)\n",
|
||||
" - [Memory](../../concepts/memory/)\n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
|
||||
"\n",
|
||||
"LangGraph allows you to persist data across **different [threads](../../concepts/persistence/#threads)**. For instance, you can store information about users (their names or preferences) in a shared (cross-thread) memory and reuse them in the new threads (e.g., new conversations).\n",
|
||||
"\n",
|
||||
"When using the [functional API](../../concepts/functional_api/), you can set it up to store and retrieve memories by using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface:\n",
|
||||
"\n",
|
||||
"1. Create an instance of a `Store`\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.store.memory import InMemoryStore, BaseStore\n",
|
||||
" \n",
|
||||
" store = InMemoryStore()\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"2. Pass the `store` instance to the `entrypoint()` decorator and expose `store` parameter in the function signature:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.func import entrypoint\n",
|
||||
"\n",
|
||||
" @entrypoint(store=store)\n",
|
||||
" def workflow(inputs: dict, store: BaseStore):\n",
|
||||
" my_task(inputs).result()\n",
|
||||
" ...\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
"In this guide, we will show how to construct and use a workflow that has a shared memory implemented using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface.\n",
|
||||
"\n",
|
||||
"!!! note Note\n",
|
||||
"\n",
|
||||
" Support for the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.32`.\n",
|
||||
"\n",
|
||||
" Support for __index__ and __query__ arguments of the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.54`.\n",
|
||||
"\n",
|
||||
"!!! tip \"Note\"\n",
|
||||
"\n",
|
||||
" If you need to add cross-thread persistence to a `StateGraph`, check out this [how-to guide](../cross-thread-persistence).\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3457aadf",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langchain_anthropic langchain_openai langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "aa2c64a7",
|
||||
"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(\"ANTHROPIC_API_KEY\")\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "51b6817d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! tip \"Set up [LangSmith](https://smith.langchain.com) for LangGraph development\"\n",
|
||||
"\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 [here](https://docs.smith.langchain.com)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6b5b3d42-3d2c-455e-ac10-e2ae74dc1cf1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: simple chatbot with long-term memory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c4c550b5-1954-496b-8b9d-800361af17dc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define store\n",
|
||||
"\n",
|
||||
"In this example we will create a workflow that will be able to retrieve information about a user's preferences. We will do so by defining an `InMemoryStore` - an object that can store data in memory and query that data.\n",
|
||||
"\n",
|
||||
"When storing objects using the `Store` interface you define two things:\n",
|
||||
"\n",
|
||||
"* the namespace for the object, a tuple (similar to directories)\n",
|
||||
"* the object key (similar to filenames)\n",
|
||||
"\n",
|
||||
"In our example, we'll be using `(\"memories\", <user_id>)` as namespace and random UUID as key for each new memory.\n",
|
||||
"\n",
|
||||
"Importantly, to determine the user, we will be passing `user_id` via the config keyword argument of the node function.\n",
|
||||
"\n",
|
||||
"Let's first define our store!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "a7f303d6-612e-4e34-bf36-29d4ed25d802",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.store.memory import InMemoryStore\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"\n",
|
||||
"in_memory_store = InMemoryStore(\n",
|
||||
" index={\n",
|
||||
" \"embed\": OpenAIEmbeddings(model=\"text-embedding-3-small\"),\n",
|
||||
" \"dims\": 1536,\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3389c9f4-226d-40c7-8bfc-ee8aac24f79d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Create workflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "2a30a362-528c-45ee-9df6-630d2d843588",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.store.base import BaseStore\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_model(messages: list[BaseMessage], memory_store: BaseStore, user_id: str):\n",
|
||||
" namespace = (\"memories\", user_id)\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" memories = memory_store.search(namespace, query=str(last_message.content))\n",
|
||||
" info = \"\\n\".join([d.value[\"data\"] for d in memories])\n",
|
||||
" system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n",
|
||||
"\n",
|
||||
" # Store new memories if the user asks the model to remember\n",
|
||||
" if \"remember\" in last_message.content.lower():\n",
|
||||
" memory = \"User name is Bob\"\n",
|
||||
" memory_store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
|
||||
"\n",
|
||||
" response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + messages)\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
|
||||
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
|
||||
"def workflow(\n",
|
||||
" inputs: list[BaseMessage],\n",
|
||||
" *,\n",
|
||||
" previous: list[BaseMessage],\n",
|
||||
" config: RunnableConfig,\n",
|
||||
" store: BaseStore,\n",
|
||||
"):\n",
|
||||
" user_id = config[\"configurable\"][\"user_id\"]\n",
|
||||
" previous = previous or []\n",
|
||||
" inputs = add_messages(previous, inputs)\n",
|
||||
" response = call_model(inputs, store, user_id).result()\n",
|
||||
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f22a4a18-67e4-4f0b-b655-a29bbe202e1c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note Note\n",
|
||||
"\n",
|
||||
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass store to the entrypoint decorator, since it's done automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "552d4e33-556d-4fa5-8094-2a076bc21529",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Run the workflow!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1842c626-6cd9-4f58-b549-58978e478098",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's specify a user ID in the config and tell the model our name:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "c871a073-a466-46ad-aafe-2b870831057e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Hello Bob! Nice to meet you. I'll remember that your name is Bob. How can I help you today?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d862be40-1f8a-4057-81c4-b7bf073dc4c1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Your name is Bob.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "80fd01ec-f135-4811-8743-daff8daea422",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now inspect our in-memory store and verify that we have in fact saved the memories for the user:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "76cde493-89cf-4709-a339-207d2b7e9ea7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'data': 'User name is Bob'}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for memory in in_memory_store.search((\"memories\", \"1\")):\n",
|
||||
" print(memory.value)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "23f5d7eb-af23-4131-b8fd-2a69e74e6e55",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now run the workflow for another user to verify that the memories about the first user are self contained:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "d362350b-d730-48bd-9652-983812fd7811",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"I don't have any information about your name. I can only see our current conversation without any prior context or personal details about you. If you'd like me to know your name, feel free to tell me!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -176,7 +176,7 @@
|
||||
" store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
|
||||
"\n",
|
||||
" response = model.invoke(\n",
|
||||
" [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
|
||||
" [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
|
||||
" )\n",
|
||||
" return {\"messages\": response}\n",
|
||||
"\n",
|
||||
@@ -240,7 +240,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -266,7 +266,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -327,7 +327,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
|
||||
+50
-19
@@ -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
|
||||
|
||||
@@ -31,6 +39,11 @@ These how-to guides show how to achieve that controllability.
|
||||
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
|
||||
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
|
||||
|
||||
See the below guides for how-to add persistence to your workflow using the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
|
||||
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
|
||||
|
||||
### Memory
|
||||
|
||||
LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) in your graph. These how-to guides show how to implement different strategies for that.
|
||||
@@ -59,6 +72,12 @@ Other methods:
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
|
||||
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
|
||||
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
|
||||
- [How to review tool calls (Functional API)](review-tool-calls-functional.ipynb)
|
||||
|
||||
### Time Travel
|
||||
|
||||
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
|
||||
@@ -69,15 +88,10 @@ Other methods:
|
||||
|
||||
[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)
|
||||
|
||||
@@ -115,6 +129,11 @@ 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 [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
|
||||
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
|
||||
|
||||
### State Management
|
||||
|
||||
- [How to use Pydantic model as graph state](state-model.ipynb)
|
||||
@@ -124,14 +143,14 @@ See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for i
|
||||
### Other
|
||||
|
||||
- [How to run graph asynchronously](async.ipynb)
|
||||
- [How to visualize your graph](visualization.ipynb)
|
||||
- [How to add runtime configuration to your graph](configuration.ipynb)
|
||||
- [How to add node retries](node-retries.ipynb)
|
||||
- [How to force tool-calling agent to structure output](react-agent-structured-output.ipynb)
|
||||
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
|
||||
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
|
||||
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
|
||||
|
||||
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
|
||||
|
||||
### Prebuilt ReAct Agent
|
||||
|
||||
The LangGraph [prebuilt ReAct agent](../reference/prebuilt.md#langgraph.prebuilt.chat_agent_executor.create_react_agent) is pre-built implementation of a [tool calling agent](../concepts/agentic_concepts.md#tool-calling-agent).
|
||||
@@ -152,6 +171,10 @@ overview of its underlying implementation to help you customize for your own nee
|
||||
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
|
||||
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
This section includes how-to guides for LangGraph Platform.
|
||||
@@ -177,6 +200,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
|
||||
- [How to test locally](../cloud/deployment/test_locally.md)
|
||||
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
|
||||
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
|
||||
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
|
||||
|
||||
### Deployment
|
||||
|
||||
@@ -266,6 +290,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
|
||||
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
|
||||
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
|
||||
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
|
||||
- [How to engineer prompts in LangGraph Studio](../cloud/how-tos/iterate_graph_studio.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -276,3 +301,9 @@ These are the guides for resolving common errors you may find while building wit
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
|
||||
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.md)
|
||||
- [INVALID_CHAT_HISTORY](../troubleshooting/errors/INVALID_CHAT_HISTORY.md)
|
||||
|
||||
### LangGraph Platform Troubleshooting
|
||||
|
||||
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
|
||||
|
||||
- [INVALID_LICENSE](../troubleshooting/errors/INVALID_LICENSE.md)
|
||||
@@ -207,7 +207,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# Here we define the logic to map out over the generated subjects\n",
|
||||
"# We will use this an edge in the graph\n",
|
||||
"# We will use this as an edge in the graph\n",
|
||||
"def continue_to_jokes(state: OverallState):\n",
|
||||
" # We will return a list of `Send` objects\n",
|
||||
" # Each `Send` object consists of the name of a node in the graph\n",
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
"source": [
|
||||
"## Using in `create_react_agent`\n",
|
||||
"\n",
|
||||
"Add semantic search to your tool calling agent by injecting the store in the `state_modifier`. You can also use the store in a tool to let your agent manually store or search for memories."
|
||||
"Add semantic search to your tool calling agent by injecting the store in the `prompt` function. You can also use the store in a tool to let your agent manually store or search for memories."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -248,9 +248,9 @@
|
||||
"agent = create_react_agent(\n",
|
||||
" init_chat_model(\"openai:gpt-4o-mini\"),\n",
|
||||
" tools=[upsert_memory],\n",
|
||||
" # The state_modifier is run to prepare the messages for the LLM. It is called\n",
|
||||
" # The 'prompt' function is run to prepare the messages for the LLM. It is called\n",
|
||||
" # right before each LLM call\n",
|
||||
" state_modifier=prepare_messages,\n",
|
||||
" prompt=prepare_messages,\n",
|
||||
" store=store,\n",
|
||||
")"
|
||||
]
|
||||
@@ -524,7 +524,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "a2b182eb-1e31-43c8-85b1-706508dfa370",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add multi-turn conversation in a multi-agent application (functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
"\n",
|
||||
" - [Multi-agent systems](../../concepts/multi_agent)\n",
|
||||
" - [Human-in-the-loop](../../concepts/human_in_the_loop)\n",
|
||||
" - [Functional API](../../concepts/functional_api)\n",
|
||||
" - [Command](../../concepts/low_level/#command)\n",
|
||||
" - [LangGraph Glossary](../../concepts/low_level/)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"In this how-to guide, we’ll build an application that allows an end-user to engage in a *multi-turn conversation* with one or more agents. We'll create a node that uses an [`interrupt`](../../reference/types/#langgraph.types.interrupt) to collect user input and routes back to the **active** agent.\n",
|
||||
"\n",
|
||||
"The agents will be implemented as tasks in a workflow that executes agent steps and determines the next action:\n",
|
||||
"\n",
|
||||
"1. **Wait for user input** to continue the conversation, or\n",
|
||||
"2. **Route to another agent** (or back to itself, such as in a loop) via a [**handoff**](../../concepts/multi_agent/#handoffs).\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"from langgraph.types import interrupt\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define a tool to signal intent to hand off to a different agent\n",
|
||||
"# Note: this is not using Command(goto) syntax for navigating to different agents:\n",
|
||||
"# `workflow()` below handles the handoffs explicitly\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define an agent\n",
|
||||
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
|
||||
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define a task that calls an agent\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define the multi-agent network workflow\n",
|
||||
"@entrypoint(checkpointer)\n",
|
||||
"def workflow(messages):\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" ai_msg = get_last_ai_msg(agent_messages)\n",
|
||||
" if not ai_msg.tool_calls:\n",
|
||||
" user_input = interrupt(value=\"Ready for user input.\")\n",
|
||||
" messages = messages + [{\"role\": \"user\", \"content\": user_input}]\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" messages = messages + agent_messages\n",
|
||||
" call_active_agent = get_next_agent(messages)\n",
|
||||
" return entrypoint.final(value=agent_messages[-1], save=messages)\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# %%capture --no-stderr\n",
|
||||
"# %pip install -U langgraph langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_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(\"ANTHROPIC_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
|
||||
"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": "c217c3fe-ca50-45a1-be91-912bc83ed8b3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
|
||||
"\n",
|
||||
"We will create 2 agents:\n",
|
||||
"\n",
|
||||
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
|
||||
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
|
||||
"\n",
|
||||
"This is a fully-connected network - every agent can talk to any other agent. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "eb51463a-4425-44ad-91d5-f21fd5b4e3b3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from typing_extensions import Literal\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_travel_recommendations():\n",
|
||||
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
|
||||
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
|
||||
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
|
||||
" return {\n",
|
||||
" \"aruba\": [\n",
|
||||
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
|
||||
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
|
||||
" ],\n",
|
||||
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
|
||||
" }[location]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_travel_advisor():\n",
|
||||
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to travel advisor\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7f5b2a7f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note \"Transfer tools\"\n",
|
||||
"\n",
|
||||
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
|
||||
" \n",
|
||||
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "213d661e-6ba4-42b9-bc7f-6c8c423e3419",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now create our agents using the the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] and our multi-agent workflow. Note that will be calling [`interrupt`][langgraph.types.interrupt] every time after we get the final response from each of the agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"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",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"# Define travel advisor ReAct agent\n",
|
||||
"travel_advisor_tools = [\n",
|
||||
" get_travel_recommendations,\n",
|
||||
" transfer_to_hotel_advisor,\n",
|
||||
"]\n",
|
||||
"travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
|
||||
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define hotel advisor ReAct agent\n",
|
||||
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
|
||||
"hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_hotel_advisor(messages):\n",
|
||||
" response = hotel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"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",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" messages = add_messages(messages, agent_messages)\n",
|
||||
" # Find the last AI message\n",
|
||||
" # If one of the handoff tools is called, the last message returned\n",
|
||||
" # by the agent will be a ToolMessage because we set them to have\n",
|
||||
" # \"return_direct=True\". This means that the last AIMessage will\n",
|
||||
" # have tool calls.\n",
|
||||
" # Otherwise, the last returned message will be an AIMessage with\n",
|
||||
" # no tool calls, which means we are ready for new input.\n",
|
||||
" 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",
|
||||
" # 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",
|
||||
" if tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
|
||||
" call_active_agent = call_hotel_advisor\n",
|
||||
" elif tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" else:\n",
|
||||
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
|
||||
"\n",
|
||||
" return entrypoint.final(value=agent_messages[-1], save=messages)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "af856e1b-41fc-4041-8cbf-3818a60088e0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Test multi-turn conversation\n",
|
||||
"\n",
|
||||
"Let's test a multi turn conversation with this application."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "2b6fde57-86e3-440e-a7bf-f1e9b5ed9ff2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"--- Conversation Turn 1 ---\n",
|
||||
"\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! 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",
|
||||
"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: I can recommend two excellent options in different areas:\n",
|
||||
"\n",
|
||||
"1. The Ritz-Carlton, Aruba - Located in Palm Beach\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",
|
||||
"- 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 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, here are some popular activities you can enjoy:\n",
|
||||
"\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",
|
||||
"Would you like more specific information about any of these activities or other suggestions in the area?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"thread_config = {\"configurable\": {\"thread_id\": uuid.uuid4()}}\n",
|
||||
"\n",
|
||||
"inputs = [\n",
|
||||
" # 1st round of conversation,\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",
|
||||
" resume=\"could you recommend a nice hotel in one of the areas and tell me which area it is.\"\n",
|
||||
" ),\n",
|
||||
" # 3rd round of conversation,\n",
|
||||
" Command(\n",
|
||||
" resume=\"i like the first one. could you recommend something to do near the hotel?\"\n",
|
||||
" ),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for idx, user_input in enumerate(inputs):\n",
|
||||
" print()\n",
|
||||
" print(f\"--- Conversation Turn {idx + 1} ---\")\n",
|
||||
" print()\n",
|
||||
" print(f\"User: {user_input}\")\n",
|
||||
" print()\n",
|
||||
" for update in multi_turn_graph.stream(\n",
|
||||
" user_input,\n",
|
||||
" config=thread_config,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
" ):\n",
|
||||
" for node_id, value in update.items():\n",
|
||||
" if isinstance(value, list) and value:\n",
|
||||
" last_message = value[-1]\n",
|
||||
" if isinstance(last_message, dict) or last_message.type != \"ai\":\n",
|
||||
" continue\n",
|
||||
" print(f\"{node_id}: {last_message.content}\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user