mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
126
Commits
cli==0.3.8
...
cli==0.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f400b38d1 | ||
|
|
6527df688c | ||
|
|
aec841bd2a | ||
|
|
2d3121a17c | ||
|
|
06f9142419 | ||
|
|
a926450601 | ||
|
|
abb96c0e2f | ||
|
|
d9e3d83894 | ||
|
|
cedecd8ed6 | ||
|
|
6bf9a7a4bc | ||
|
|
bce1dcfcd2 | ||
|
|
9b46cba1fb | ||
|
|
a6dab889d1 | ||
|
|
7c6dbb3972 | ||
|
|
e08b8352a8 | ||
|
|
f7fe7c6698 | ||
|
|
c2bc6ab8e9 | ||
|
|
420550501f | ||
|
|
a0599139b8 | ||
|
|
c2f359f708 | ||
|
|
7f78a011fd | ||
|
|
7d166bfb9f | ||
|
|
6cc8899818 | ||
|
|
1ba96f49bf | ||
|
|
b0958115c1 | ||
|
|
04fb14d3ae | ||
|
|
efb0e8c176 | ||
|
|
0584eaa5c4 | ||
|
|
0c73af5624 | ||
|
|
9d1bb9d86c | ||
|
|
7c69cb54a6 | ||
|
|
c2279cbe6f | ||
|
|
3a024cff6d | ||
|
|
4a0b2fa0ef | ||
|
|
36179ab1d2 | ||
|
|
20ddb2b8b4 | ||
|
|
b0a25f2794 | ||
|
|
ea0aebaa2e | ||
|
|
26c68aa528 | ||
|
|
4101aebeea | ||
|
|
90ac06deb6 | ||
|
|
32d66d48eb | ||
|
|
d933d455ec | ||
|
|
c421afba65 | ||
|
|
6139dacef9 | ||
|
|
affaa90d2a | ||
|
|
9f969f5fe1 | ||
|
|
fb531b2473 | ||
|
|
fe4029b3b8 | ||
|
|
7cd9a8e5dd | ||
|
|
5ba02d5b46 | ||
|
|
11834512db | ||
|
|
eeb731c07e | ||
|
|
f0fced262a | ||
|
|
8dc4465d05 | ||
|
|
d0a3eaf601 | ||
|
|
6f45f13952 | ||
|
|
328129e5bd | ||
|
|
2d05a17dfb | ||
|
|
5a36229e38 | ||
|
|
eeadeb282e | ||
|
|
3a22aa0af3 | ||
|
|
9467a0e2bb | ||
|
|
8b55dff7a5 | ||
|
|
a19b74154a | ||
|
|
0607dc4611 | ||
|
|
a3ee814539 | ||
|
|
b65140a892 | ||
|
|
77a63608d1 | ||
|
|
c6179ca9d5 | ||
|
|
f087567853 | ||
|
|
bdef6b3f5d | ||
|
|
677d941bb6 | ||
|
|
a43acc33bd | ||
|
|
326fd55e4f | ||
|
|
6037f0210f | ||
|
|
d9328027f9 | ||
|
|
7170e04aa6 | ||
|
|
20581e61c0 | ||
|
|
4af07942ed | ||
|
|
682f39e0d3 | ||
|
|
7bbe8d8628 | ||
|
|
4dfd1c368c | ||
|
|
b75daf093e | ||
|
|
faacbc1570 | ||
|
|
f5f536ba78 | ||
|
|
7284326160 | ||
|
|
62f7548532 | ||
|
|
e6a9e1d1c1 | ||
|
|
94fa329100 | ||
|
|
264caa0684 | ||
|
|
cd33de2ad1 | ||
|
|
692f177a10 | ||
|
|
14d4be6c4a | ||
|
|
2e133d6189 | ||
|
|
89de950307 | ||
|
|
788b62c0fb | ||
|
|
1539a55d2c | ||
|
|
f3055178f3 | ||
|
|
c0067cd304 | ||
|
|
b90d7c4e58 | ||
|
|
2c14b1d658 | ||
|
|
50ce6badee | ||
|
|
6fc5b3aeda | ||
|
|
b543752878 | ||
|
|
8f6ad0b25a | ||
|
|
ada5d2ecb1 | ||
|
|
eaeafe54ab | ||
|
|
f761116de7 | ||
|
|
36cf353d19 | ||
|
|
d503c0bf33 | ||
|
|
25ba4c3bda | ||
|
|
dfc1c59ebf | ||
|
|
6f4c5fefee | ||
|
|
7cf230defa | ||
|
|
5db65e0281 | ||
|
|
b08c2e092f | ||
|
|
120ae38c12 | ||
|
|
22942d4eec | ||
|
|
1756ce1dd2 | ||
|
|
0b4638269b | ||
|
|
1ebdb1ba31 | ||
|
|
f3423c052e | ||
|
|
b63572ee16 | ||
|
|
ddf4e62bde | ||
|
|
d73902ae76 |
@@ -1,6 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
version: 2.1
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
|
||||
about: Report an issue related to the LangGraph documentation
|
||||
- name: LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: General community discussions, support, and feature requests
|
||||
about: General community discussions and support
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Documentation
|
||||
description: Report an issue related to the LangGraph documentation.
|
||||
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||
labels: [documentation]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Issue with current documentation:"
|
||||
description: >
|
||||
Please make sure to leave a reference to the document/code you're
|
||||
referring to.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Idea or request for content:"
|
||||
description: >
|
||||
Please describe as clearly as possible what topics you think are missing
|
||||
from the current documentation.
|
||||
@@ -14,12 +14,25 @@ jobs:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
example:
|
||||
- name: A
|
||||
workdir: libs/cli/examples
|
||||
tag: langgraph-test-a
|
||||
- name: B
|
||||
workdir: libs/cli/examples/graphs
|
||||
tag: langgraph-test-b
|
||||
- name: C
|
||||
workdir: libs/cli/examples/graphs_reqs_a
|
||||
tag: langgraph-test-c
|
||||
- name: D
|
||||
workdir: libs/cli/examples/graphs_reqs_b
|
||||
tag: langgraph-test-d
|
||||
name: "CLI integration test"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: libs/cli
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
@@ -27,63 +40,71 @@ jobs:
|
||||
filter: "libs/cli/**"
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.changed-files.outputs.all
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
cache-suffix: "cli-integration-test"
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Setup env
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples
|
||||
run: cat .env.example > .env
|
||||
- name: Install cli globally
|
||||
if: steps.changed-files.outputs.all
|
||||
run: pip install -e .
|
||||
- name: Build and test service A
|
||||
- name: Build and test service ${{ matrix.example.name }}
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
# The build-arg isn't used; just testing that we accept other args
|
||||
langgraph build -t langgraph-test-a
|
||||
cp .env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
|
||||
- name: Build and test service B
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-b
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
|
||||
- name: Build and test service C
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_a
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-c
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
|
||||
- name: Build and test service D
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_b
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-d
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
|
||||
# Build the image for this example
|
||||
langgraph build -t ${{ matrix.example.tag }}
|
||||
# Prepare environment file from local or parent example directory
|
||||
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi; fi
|
||||
# Run the integration test using the built tag
|
||||
# Compute repo root to reference the shared script robustly
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
|
||||
|
||||
- name: Build JS service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/js-examples
|
||||
run: |
|
||||
langgraph build -t langgraph-test-e
|
||||
|
||||
- name: Build JS monorepo service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/js-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
|
||||
|
||||
- name: Build Python monorepo service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/python-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
cp apps/agent/.env.example apps/agent/.env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env; fi
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
|
||||
- name: Build and test prerelease reqs service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||
run: |
|
||||
langgraph build -t langgraph-test-h
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
|
||||
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
|
||||
if [ "$LANGGRAPH_VERSION" != "1.0.0a2" ]; then
|
||||
exit 1
|
||||
fi
|
||||
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
|
||||
if [ "$LANGCHAIN_OPENAI_VERSION" != "0.3.0" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test prerelease reqs fail service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
||||
run: |
|
||||
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- "3.12"
|
||||
name: "lint #${{ matrix.python-version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
filter: "${{ inputs.working-directory }}/**"
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.changed-files.outputs.all
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
|
||||
@@ -17,7 +17,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
@@ -25,9 +24,9 @@ jobs:
|
||||
|
||||
name: "test #${{ matrix.python-version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
|
||||
@@ -12,7 +12,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
@@ -23,9 +22,9 @@ jobs:
|
||||
working-directory: libs/langgraph
|
||||
name: "test #${{ matrix.python-version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
|
||||
@@ -16,7 +16,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
@@ -24,10 +23,10 @@ jobs:
|
||||
version: ${{ steps.check-version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python $${ env.PYTHON_VERSION }}
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
enable-cache: true
|
||||
@@ -75,9 +74,9 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: test-dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
run:
|
||||
working-directory: libs/langgraph
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
|
||||
- name: Set up Python 3.11
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
|
||||
@@ -15,14 +15,14 @@ jobs:
|
||||
run:
|
||||
working-directory: libs/langgraph
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- id: files
|
||||
name: Get changed files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
format: json
|
||||
- name: Set up Python 3.11
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Annotation
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
deps: ${{ steps.filter.outputs.deps }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
name: "Check SDK methods matching"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Run check_sdk_methods script
|
||||
@@ -118,9 +118,9 @@ jobs:
|
||||
python-version:
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
name: Deploy Docs
|
||||
name: Deploy Docs Redirects
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -23,30 +20,18 @@ defaults:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed-files: ${{ steps.changed-files.outputs.added_modified }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
filter: "docs/docs/**"
|
||||
|
||||
deploy:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
enable-cache: true
|
||||
@@ -62,85 +47,22 @@ jobs:
|
||||
uv run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
fi
|
||||
|
||||
- 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: |
|
||||
# If this is main branch, then we want to download stats. we do this
|
||||
# with the env variable DOWNLOAD_STATS=true
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
DOWNLOAD_STATS=true make build-docs
|
||||
else
|
||||
make build-docs
|
||||
fi
|
||||
|
||||
- name: Build site (redirects only)
|
||||
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
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "schedule" ]; then
|
||||
echo "Running link check on all HTML files matching notebooks in docs directory..."
|
||||
uv run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
--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-ignore "docs/docs/static/wordmark_*" \
|
||||
--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/|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..."
|
||||
uv run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links ${CHANGED_FILES} \
|
||||
|| ([ $? = 5 ] && exit 0 || exit $?)
|
||||
else
|
||||
echo "No notebook files changed."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ./docs/site/
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate PR Title
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
sdk-py
|
||||
docs
|
||||
ci
|
||||
deps
|
||||
requireScope: false
|
||||
ignoreLabels: |
|
||||
ignore-lint-pr-title
|
||||
|
||||
@@ -16,7 +16,6 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
@@ -26,10 +25,10 @@ jobs:
|
||||
tag: ${{ steps.check-version.outputs.tag }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
enable-cache: true
|
||||
@@ -87,7 +86,7 @@ jobs:
|
||||
outputs:
|
||||
release-body: ${{ steps.generate-release-body.outputs.release-body }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
repository: langchain-ai/langgraph
|
||||
path: langgraph
|
||||
@@ -158,7 +157,7 @@ jobs:
|
||||
- test-pypi-publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
# We explicitly *don't* set up caching here. This ensures our tests are
|
||||
# maximally sensitive to catching breakage.
|
||||
@@ -174,7 +173,7 @@ jobs:
|
||||
# used in the real world.
|
||||
|
||||
- name: Set up Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
enable-cache: true
|
||||
@@ -261,16 +260,16 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
enable-cache: true
|
||||
cache-suffix: "release"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -302,16 +301,16 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
enable-cache: true
|
||||
cache-suffix: "release"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
- "latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python + Poetry
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
# use minimum supported Python version
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
enable-cache: true
|
||||
cache-suffix: "uv-lock-upgrade"
|
||||
|
||||
@@ -33,8 +33,8 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||
title: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||
commit-message: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
||||
title: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
||||
body: |
|
||||
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Below is a high-level overview:
|
||||
- **langgraph** – core framework for building stateful, multi-actor agents.
|
||||
- **prebuilt** – high-level APIs for creating and running agents and tools.
|
||||
- **sdk-js** – JS/TS SDK for interacting with the LangGraph REST API.
|
||||
- **sdk-py** – Python SDK for the LangGraph Platform API.
|
||||
- **sdk-py** – Python SDK for the LangGraph Server API.
|
||||
|
||||
### Dependency map
|
||||
|
||||
|
||||
+3
-3
@@ -277,9 +277,9 @@ def my_function(arg1: int, arg2: str) -> float:
|
||||
Examples:
|
||||
This is a section for examples of how to use the function.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_function(1, "hello")
|
||||
```python
|
||||
my_function(1, "hello")
|
||||
\```
|
||||
|
||||
Args:
|
||||
arg1: This is a description of arg1. We do not need to specify the type since
|
||||
|
||||
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
- [LangChain](https://python.langchain.com/docs/introduction/) – Provides integrations and composable components to streamline LLM application development.
|
||||
|
||||
> [!NOTE]
|
||||
@@ -71,7 +71,7 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
|
||||
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
|
||||
|
||||
+113
-10
@@ -1,24 +1,126 @@
|
||||
# Setup
|
||||
# LangGraph Documentation
|
||||
|
||||
To setup requirements for building docs you can run:
|
||||
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md).
|
||||
|
||||
```bash
|
||||
uv sync --group test
|
||||
## Structure
|
||||
|
||||
The primary documentation is located in the `docs/` directory. This directory contains both the source files for the main documentation as well as the API reference doc build process.
|
||||
|
||||
### Main Documentation
|
||||
|
||||
Main documentation files are located in `docs/docs/` and are written in Markdown format. The site uses [**MkDocs**](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) and includes:
|
||||
|
||||
- **Concepts**: Core LangGraph concepts and explanations
|
||||
- **Tutorials**: Step-by-step learning guides
|
||||
- **How-tos**: Task-focused guides for specific use cases
|
||||
- **Examples**: Real-world applications and use cases
|
||||
- **Jupyter Notebooks**: Interactive tutorials that are automatically converted to markdown
|
||||
|
||||
### API Reference
|
||||
|
||||
API reference documentation is defined in `docs/docs/reference/`. Each `.md` file outlines the "template" that each page is built from. Reference content is automatically generated from docstrings in the codebase using the **mkdocstrings** plugin. Once generated, the content is plugged into the corresponding markdown file where it is referenced by using manual directives to specify which classes and/or functions are documented:
|
||||
|
||||
```markdown
|
||||
::: langgraph.graph.state.StateGraph
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
show_root_heading: true
|
||||
show_root_full_path: false
|
||||
members:
|
||||
- add_node
|
||||
- add_edge
|
||||
- add_conditional_edges
|
||||
- add_sequence
|
||||
- compile
|
||||
```
|
||||
|
||||
## Serving documentation locally
|
||||
## Build Process
|
||||
|
||||
To run the documentation server locally you can run:
|
||||
Docs are built following these steps:
|
||||
|
||||
1. **Content Processing:**
|
||||
- `_scripts/notebook_hooks.py` - Main processing pipeline that:
|
||||
- Converts how-tos/tutorial Jupyter notebooks to markdown using `notebook_convert.py`
|
||||
- Adds automatic API reference links to code blocks using `generate_api_reference_links.py`
|
||||
- Handles conditional rendering for Python/JS versions
|
||||
- Processes highlight comments and custom syntax
|
||||
|
||||
2. **API Reference Generation:**
|
||||
- **mkdocstrings** plugin extracts docstrings from Python source code
|
||||
- Manual `::: module.Class` directives in reference pages (`/docs/docs/*`) specify what to document
|
||||
- Cross-references are automatically generated between docs and API
|
||||
|
||||
3. **Site Generation:**
|
||||
- **MkDocs** processes all markdown files and generates static HTML
|
||||
- Custom hooks handle redirects and inject additional functionality
|
||||
|
||||
4. **Deployment:**
|
||||
- Site is deployed with Vercel
|
||||
- `make build-docs` generates production build (also usable for local testing)
|
||||
- Automatic redirects handle URL changes between versions
|
||||
|
||||
### Local Development
|
||||
|
||||
For local development, use the Makefile targets:
|
||||
|
||||
```bash
|
||||
# Serve docs locally with hot reloading
|
||||
make serve-docs
|
||||
|
||||
# Clean build for production testing
|
||||
make build-docs
|
||||
|
||||
# Serve with clean build
|
||||
make serve-clean-docs
|
||||
```
|
||||
|
||||
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
|
||||
The `serve-docs` command:
|
||||
|
||||
- Watches source files for changes
|
||||
- Includes dirty builds for faster iteration
|
||||
- Serves on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/)
|
||||
|
||||
## Standards
|
||||
|
||||
**Docstring Format:**
|
||||
The API reference uses **Google-style docstrings** with Markdown markup. The `mkdocstrings` plugin processes these to generate documentation.
|
||||
|
||||
**Required format:**
|
||||
|
||||
```python
|
||||
def example_function(param1: str, param2: int = 5) -> bool:
|
||||
"""Brief description of the function.
|
||||
|
||||
Longer description can go here. Use Markdown syntax for
|
||||
rich formatting like **bold** and *italic*.
|
||||
|
||||
Args:
|
||||
param1: Description of the first parameter.
|
||||
param2: Description of the second parameter with default value.
|
||||
|
||||
Returns:
|
||||
Description of the return value.
|
||||
|
||||
Raises:
|
||||
ValueError: When param1 is empty.
|
||||
TypeError: When param2 is not an integer.
|
||||
|
||||
!!! warning
|
||||
This function is experimental and may change.
|
||||
|
||||
!!! version-added "Added in version 0.2.0"
|
||||
"""
|
||||
```
|
||||
|
||||
**Special Markers:**
|
||||
|
||||
- **MkDocs admonitions**: `!!! warning`, `!!! note`, `!!! version-added`
|
||||
- **Code blocks**: Standard markdown ``` syntax
|
||||
- **Cross-references**: Automatic linking via `generate_api_reference_links.py`
|
||||
|
||||
## Execute notebooks
|
||||
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run:
|
||||
|
||||
```bash
|
||||
python _scripts/prepare_notebooks_for_ci.py
|
||||
@@ -33,8 +135,9 @@ python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
```
|
||||
|
||||
`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
|
||||
|
||||
- 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
|
||||
|
||||
## Adding new notebooks
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Generate API reference links for imports in Python code blocks within markdown files."""
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import logging
|
||||
@@ -70,8 +72,18 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
|
||||
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
|
||||
# other prebuilts
|
||||
(["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
|
||||
(["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
|
||||
(
|
||||
["langgraph_supervisor"],
|
||||
"langgraph_supervisor.supervisor",
|
||||
"create_supervisor",
|
||||
"supervisor",
|
||||
),
|
||||
(
|
||||
["langgraph_supervisor"],
|
||||
"langgraph_supervisor.handoff",
|
||||
"create_handoff_tool",
|
||||
"supervisor",
|
||||
),
|
||||
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
|
||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
|
||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
|
||||
|
||||
@@ -29,7 +29,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _transform_link(
|
||||
link_name: str, scope: str, file_path: str, line_number: int, custom_title: Optional[str] = None
|
||||
link_name: str,
|
||||
scope: str,
|
||||
file_path: str,
|
||||
line_number: int,
|
||||
custom_title: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""Transform a cross-reference link based on the current scope.
|
||||
|
||||
@@ -38,7 +42,7 @@ def _transform_link(
|
||||
scope: The current scope context ("global", "python", "js", etc.).
|
||||
file_path: The file path for error reporting.
|
||||
line_number: The line number for error reporting.
|
||||
custom_title: Optional custom title for the link. If None, uses link_name.
|
||||
custom_title: Optional custom title for the link. If `None`, uses link_name.
|
||||
|
||||
Returns:
|
||||
A formatted markdown link if the link is found in the scope mapping,
|
||||
@@ -117,7 +121,9 @@ CROSS_REFERENCE_PATTERN = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _replace_autolinks(markdown: str, file_path: str, *, default_scope: str = "python") -> str:
|
||||
def _replace_autolinks(
|
||||
markdown: str, file_path: str, *, default_scope: str = "python"
|
||||
) -> str:
|
||||
"""Preprocess markdown lines to handle @[links] with conditional fence scopes.
|
||||
|
||||
This function processes markdown content to transform @[link_name] references
|
||||
@@ -169,7 +175,7 @@ def _replace_autolinks(markdown: str, file_path: str, *, default_scope: str = "p
|
||||
# This is @[ref] format
|
||||
link_name = match.group("link_name")
|
||||
custom_title = None
|
||||
|
||||
|
||||
transformed = _transform_link(
|
||||
link_name, current_scope, file_path, line_number, custom_title
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -2108,9 +2108,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"hono@npm:^4.5.4":
|
||||
version: 4.8.9
|
||||
resolution: "hono@npm:4.8.9"
|
||||
checksum: 10c0/385539d1787fdc747bc869ef0e5ccc9f39cbe40289b94f23eecfc82c6ca440f059704647cd6381a5066d2cf7baa43ab25184c78d44af4c5c98a5c5b07670059e
|
||||
version: 4.9.7
|
||||
resolution: "hono@npm:4.9.7"
|
||||
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Convert Jupyter notebooks to markdown with custom processing."""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
|
||||
+444
-170
@@ -27,185 +27,413 @@ DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
||||
|
||||
REDIRECT_MAP = {
|
||||
# lib redirects
|
||||
"how-tos/stream-values.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||
"how-tos/stream-updates.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||
"how-tos/streaming-content.ipynb": "how-tos/streaming.md",
|
||||
"how-tos/stream-multiple.ipynb": "how-tos/streaming.md#stream-multiple-nodes",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming.md#use-with-any-llm",
|
||||
"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",
|
||||
"how-tos/stream-values.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/stream-updates.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-content.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/stream-multiple.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-from-final-node.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
# graph-api
|
||||
"how-tos/state-reducers.ipynb": "how-tos/graph-api.md#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "how-tos/graph-api.md#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "how-tos/graph-api.md#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "how-tos/graph-api.md#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "how-tos/graph-api.md#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "how-tos/graph-api.md#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "how-tos/graph-api.md#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "how-tos/graph-api.md#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "how-tos/graph-api.md#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "how-tos/graph-api.md#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "how-tos/graph-api.md#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "how-tos/graph-api.md#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "how-tos/graph-api.md#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "how-tos/graph-api.md#async",
|
||||
"how-tos/state-reducers.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#async",
|
||||
# memory how-tos
|
||||
"how-tos/memory/manage-conversation-history.ipynb": "how-tos/memory/add-memory.md",
|
||||
"how-tos/memory/delete-messages.ipynb": "how-tos/memory/add-memory.md#delete-messages",
|
||||
"how-tos/memory/add-summary-conversation-history.ipynb": "how-tos/memory/add-memory.md#summarize-messages",
|
||||
"how-tos/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
"agents/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
"how-tos/memory/manage-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/memory/delete-messages.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#delete-messages",
|
||||
"how-tos/memory/add-summary-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#summarize-messages",
|
||||
"how-tos/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
# subgraph how-tos
|
||||
"how-tos/subgraph-transform-state.ipynb": "how-tos/subgraph.md#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "how-tos/subgraph.md#add-persistence",
|
||||
"how-tos/subgraph-transform-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#add-persistence",
|
||||
# persistence how-tos
|
||||
"how-tos/persistence_postgres.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/persistence_mongodb.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/persistence_redis.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/subgraph-persistence.ipynb": "how-tos/memory/add-memory.md#use-with-subgraphs",
|
||||
"how-tos/cross-thread-persistence.ipynb": "how-tos/memory/add-memory.md#add-long-term-memory",
|
||||
"cloud/how-tos/copy_threads": "cloud/how-tos/use_threads",
|
||||
"cloud/how-tos/check-thread-status": "cloud/how-tos/use_threads",
|
||||
"cloud/concepts/threads.md": "concepts/persistence.md#threads",
|
||||
"how-tos/persistence.ipynb": "how-tos/memory/add-memory.md",
|
||||
"how-tos/persistence_postgres.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/persistence_mongodb.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/persistence_redis.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/subgraph-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-with-subgraphs",
|
||||
"how-tos/cross-thread-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
|
||||
"cloud/how-tos/copy_threads": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/check-thread-status": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/concepts/threads.md": "https://docs.langchain.com/oss/python/langgraph/persistence#threads",
|
||||
"how-tos/persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
# tool calling how-tos
|
||||
"how-tos/tool-calling-errors.ipynb": "how-tos/tool-calling.ipynb#handle-errors",
|
||||
"how-tos/pass-config-to-tools.ipynb": "how-tos/tool-calling.ipynb#access-config",
|
||||
"how-tos/pass-run-time-values-to-tools.ipynb": "how-tos/tool-calling.ipynb#read-state",
|
||||
"how-tos/update-state-from-tools.ipynb": "how-tos/tool-calling.ipynb#update-state",
|
||||
"agents/tools.md": "how-tos/tool-calling.md",
|
||||
"how-tos/tool-calling-errors.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/pass-config-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/pass-run-time-values-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/update-state-from-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"agents/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
# multi-agent how-tos
|
||||
"how-tos/agent-handoffs.ipynb": "how-tos/multi_agent.md#handoffs",
|
||||
"how-tos/multi-agent-network.ipynb": "how-tos/multi_agent.md#use-in-a-multi-agent-system",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "how-tos/multi_agent.md#multi-turn-conversation",
|
||||
"how-tos/agent-handoffs.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/multi-agent-network.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "index.md",
|
||||
"cloud/how-tos/index.md": "concepts/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",
|
||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"cloud/how-tos/human_in_the_loop_user_input.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"concepts/platform_architecture.md": "concepts/langgraph_cloud#architecture",
|
||||
"cloud/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"cloud/how-tos/index.md": "https://docs.langchain.com/langsmith/home",
|
||||
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"cloud/concepts/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/faq/studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"cloud/how-tos/human_in_the_loop_user_input.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"concepts/platform_architecture.md": "https://docs.langchain.com/langsmith/cloud#architecture",
|
||||
# cloud streaming redirects
|
||||
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/concepts/streaming.md": "concepts/streaming.md",
|
||||
"agents/streaming.md": "how-tos/streaming.md",
|
||||
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"agents/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
# prebuilt redirects
|
||||
"how-tos/create-react-agent.ipynb": "agents/agents.md#basic-configuration",
|
||||
"how-tos/create-react-agent-memory.ipynb": "agents/memory.md",
|
||||
"how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts",
|
||||
"how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output",
|
||||
"how-tos/create-react-agent.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#basic-configuration",
|
||||
"how-tos/create-react-agent-memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/create-react-agent-system-prompt.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/create-react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
|
||||
# misc
|
||||
"prebuilt.md": "agents/prebuilt.md",
|
||||
"reference/prebuilt.md": "reference/agents.md",
|
||||
"concepts/high_level.md": "index.md",
|
||||
"concepts/index.md": "index.md",
|
||||
"concepts/v0-human-in-the-loop.md": "concepts/human-in-the-loop.md",
|
||||
"how-tos/index.md": "index.md",
|
||||
"tutorials/introduction.ipynb": "concepts/why-langgraph.md",
|
||||
"agents/deployment.md": "tutorials/langgraph-platform/local-server.md",
|
||||
"prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"reference/prebuilt.md": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"concepts/high_level.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/v0-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/introduction.ipynb": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/deployment.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
# deployment redirects
|
||||
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
|
||||
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
|
||||
"tutorials/deployment.md": "concepts/deployment_options.md",
|
||||
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"tutorials/deployment.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
# assistant redirects
|
||||
"cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md",
|
||||
"cloud/concepts/runs.md": "concepts/assistants.md#execution",
|
||||
"cloud/how-tos/assistant_versioning.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"cloud/concepts/runs.md": "https://docs.langchain.com/langsmith/assistants#execution",
|
||||
# hitl redirects
|
||||
"how-tos/wait-user-input-functional.ipynb": "how-tos/use-functional-api.md",
|
||||
"how-tos/review-tool-calls-functional.ipynb": "how-tos/use-functional-api.md",
|
||||
"how-tos/create-react-agent-hitl.ipynb": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"agents/human-in-the-loop.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "how-tos/human_in_the_loop/breakpoints.md",
|
||||
"concepts/breakpoints.md": "concepts/human_in_the_loop.md",
|
||||
"how-tos/human_in_the_loop/breakpoints.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"cloud/how-tos/human_in_the_loop_breakpoint.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
|
||||
"how-tos/wait-user-input-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/review-tool-calls-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/create-react-agent-hitl.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"agents/human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"cloud/how-tos/human_in_the_loop_breakpoint.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
|
||||
# LGP mintlify migration redirects
|
||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langgraph-platform/auth",
|
||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langgraph-platform/resource-auth",
|
||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langgraph-platform/add-auth-server",
|
||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langgraph-platform/use-remote-graph",
|
||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langgraph-platform/autogen-integration",
|
||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langgraph-platform/use-stream-react",
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langgraph-platform/generative-ui-react",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langgraph-platform/index",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langgraph-platform/components",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langgraph-platform/langgraph-server",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langgraph-platform/data-plane",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langgraph-platform/control-plane",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
|
||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langgraph-platform/datasets-studio",
|
||||
"concepts/sdk.md": "https://docs.langchain.com/langgraph-platform/sdk",
|
||||
"concepts/plans.md": "https://docs.langchain.com/langgraph-platform/plans",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langgraph-platform/application-structure",
|
||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langgraph-platform/scalability-and-resilience",
|
||||
"concepts/auth.md": "https://docs.langchain.com/langgraph-platform/auth",
|
||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langgraph-platform/custom-auth",
|
||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langgraph-platform/openapi-security",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langgraph-platform/assistants",
|
||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langgraph-platform/configuration-cloud",
|
||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langgraph-platform/use-threads",
|
||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langgraph-platform/background-run",
|
||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langgraph-platform/same-thread",
|
||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langgraph-platform/stateless-runs",
|
||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langgraph-platform/configurable-headers",
|
||||
"concepts/double_texting.md": "https://docs.langchain.com/langgraph-platform/double-texting",
|
||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langgraph-platform/interrupt-concurrent",
|
||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langgraph-platform/rollback-concurrent",
|
||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langgraph-platform/reject-concurrent",
|
||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langgraph-platform/enqueue-concurrent",
|
||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langgraph-platform/custom-lifespan",
|
||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langgraph-platform/custom-middleware",
|
||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langgraph-platform/custom-routes",
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langgraph-platform/data-storage-and-privacy",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langgraph-platform/semantic-search",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langgraph-platform/configure-ttl",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langgraph-platform/deployment-options",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langgraph-platform/deployment-quickstart",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langgraph-platform/setup-app-requirements-txt",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langgraph-platform/setup-pyproject",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langgraph-platform/setup-javascript",
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langgraph-platform/custom-docker",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langgraph-platform/graph-rebuild",
|
||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#standalone-server",
|
||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform",
|
||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-standalone-server",
|
||||
"concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp",
|
||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel",
|
||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langgraph-platform/server-api-ref",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langgraph-platform/langgraph-server-changelog",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langgraph-platform/api-ref-control-plane",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langgraph-platform/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langgraph-platform/troubleshooting-studio",
|
||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
|
||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
|
||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
|
||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
|
||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio#run-application",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio#manage-assistants",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-studio#manage-threads",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/observability-studio#iterate-on-prompts",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability-studio#run-experiments-over-a-dataset",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability-studio#debug-langsmith-traces",
|
||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/observability-studio#add-node-to-dataset",
|
||||
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
|
||||
"concepts/plans.md": "https://langchain.com/pricing",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
||||
"concepts/auth.md": "https://docs.langchain.com/langsmith/authentication-methods",
|
||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
|
||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
|
||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
|
||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
|
||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
|
||||
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
|
||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
|
||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
|
||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
|
||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hybrid",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/self-hosted",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/self-hosted#standalone-server",
|
||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/deploy-hybrid",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/deploy-self-hosted-full-platform",
|
||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/deploy-standalone-server",
|
||||
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
|
||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
||||
|
||||
# LangGraph mintlify migration redirects
|
||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/get-started/1-build-basic-chatbot.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/2-add-tools.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/3-add-memory.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/4-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/5-customize-state.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"reference/index.md": "https://reference.langchain.com/python/langgraph/",
|
||||
"reference/graphs.md": "https://reference.langchain.com/python/langgraph/graphs/",
|
||||
"reference/func.md": "https://reference.langchain.com/python/langgraph/func/",
|
||||
"reference/pregel.md": "https://reference.langchain.com/python/langgraph/pregel/",
|
||||
"reference/checkpoints.md": "https://reference.langchain.com/python/langgraph/checkpoints/",
|
||||
"reference/store.md": "https://reference.langchain.com/python/langgraph/store/",
|
||||
"reference/cache.md": "https://reference.langchain.com/python/langgraph/cache/",
|
||||
"reference/types.md": "https://reference.langchain.com/python/langgraph/types/",
|
||||
"reference/runtime.md": "https://reference.langchain.com/python/langgraph/runtime/",
|
||||
"reference/config.md": "https://reference.langchain.com/python/langgraph/config/",
|
||||
"reference/errors.md": "https://reference.langchain.com/python/langgraph/errors/",
|
||||
"reference/constants.md": "https://reference.langchain.com/python/langgraph/constants/",
|
||||
"reference/channels.md": "https://reference.langchain.com/python/langgraph/channels/",
|
||||
"reference/agents.md": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
|
||||
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
|
||||
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
|
||||
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
|
||||
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
|
||||
|
||||
# additional exclude-search entries from mkdocs.yml
|
||||
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability",
|
||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
|
||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
|
||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
|
||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
|
||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
|
||||
"cloud/reference/sdk/js_ts_sdk_ref.md": "https://reference.langchain.com/javascript/modules/langsmith.html",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
||||
"concepts/auth.md": "https://docs.langchain.com/langsmith/auth",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
|
||||
"concepts/faq.md": "https://docs.langchain.com/langsmith/faq",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/home",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"concepts/plans.md": "https://docs.langchain.com/langsmith/home",
|
||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
||||
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
|
||||
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
|
||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
|
||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
|
||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
|
||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
|
||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
|
||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"snippets/chat_model_tabs.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
|
||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
|
||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
}
|
||||
|
||||
|
||||
@@ -560,10 +788,16 @@ def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
site_dir = config["site_dir"]
|
||||
|
||||
# Track which paths have explicit redirects
|
||||
redirected_paths = set()
|
||||
|
||||
# Process explicit redirects from REDIRECT_MAP
|
||||
for page_old, page_new in REDIRECT_MAP.items():
|
||||
# Convert .ipynb to .md for path calculation
|
||||
page_old = page_old.replace(".ipynb", ".md")
|
||||
|
||||
|
||||
# Calculate the HTML path for the old page (whether it exists or not)
|
||||
if use_directory_urls:
|
||||
# With directory URLs: /path/to/page/ becomes /path/to/page/index.html
|
||||
@@ -577,15 +811,18 @@ def on_post_build(config):
|
||||
old_html_path = page_old[:-3] + ".html"
|
||||
else:
|
||||
old_html_path = page_old + ".html"
|
||||
|
||||
|
||||
# Track this path as redirected
|
||||
redirected_paths.add(old_html_path)
|
||||
|
||||
if isinstance(page_new, str) and page_new.startswith("http"):
|
||||
# Handle external redirects
|
||||
_write_html(config["site_dir"], old_html_path, page_new)
|
||||
_write_html(site_dir, old_html_path, page_new)
|
||||
else:
|
||||
# Handle internal redirects
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
|
||||
|
||||
# Try to get the new path using File class, but fallback to manual calculation
|
||||
try:
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
@@ -607,5 +844,42 @@ def on_post_build(config):
|
||||
else:
|
||||
new_html_path = page_new_before_hash + ".html"
|
||||
new_html_path += hash + suffix
|
||||
|
||||
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
_write_html(site_dir, old_html_path, new_html_path)
|
||||
|
||||
# Create root index.html redirect
|
||||
root_redirect_html = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting to LangGraph Documentation</title>
|
||||
<link rel="canonical" href="https://docs.langchain.com/oss/python/langgraph/overview">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="https://docs.langchain.com/oss/python/langgraph/overview"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url=https://docs.langchain.com/oss/python/langgraph/overview">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Documentation has moved</h1>
|
||||
<p>The LangGraph documentation has moved to <a href="https://docs.langchain.com/oss/python/langgraph/overview">docs.langchain.com</a>.</p>
|
||||
<p>Redirecting you now...</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
root_index_path = os.path.join(site_dir, "index.html")
|
||||
with open(root_index_path, "w", encoding="utf-8") as f:
|
||||
f.write(root_redirect_html)
|
||||
|
||||
# Create server-side catch-all redirect file for Netlify/Cloudflare Pages
|
||||
# This handles any pages not explicitly mapped in REDIRECT_MAP
|
||||
redirects_content = """# Netlify/Cloudflare Pages redirect rules
|
||||
# Specific redirects are handled by individual HTML redirect pages
|
||||
# This is the catch-all for any unmapped pages
|
||||
|
||||
# Catch-all: redirect any page not explicitly mapped
|
||||
/* https://docs.langchain.com/oss/python/langgraph/overview 301
|
||||
"""
|
||||
|
||||
redirects_path = os.path.join(site_dir, "_redirects")
|
||||
with open(redirects_path, "w", encoding="utf-8") as f:
|
||||
f.write(redirects_content)
|
||||
|
||||
@@ -20,16 +20,19 @@ class Package(TypedDict):
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
class ResolvedPackage(Package):
|
||||
weekly_downloads: int | None
|
||||
"""The weekly download count of the package."""
|
||||
language: str
|
||||
"""The language of the package. (either 'python' or 'js')"""
|
||||
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
PACKAGES_FILE = HERE / "packages.yml"
|
||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())["packages"]
|
||||
|
||||
|
||||
def _get_pypi_downloads(package: Package) -> int:
|
||||
"""Retrieve the weekly download count for a package from PyPIStats."""
|
||||
|
||||
@@ -72,7 +75,8 @@ def _get_pypi_downloads(package: Package) -> int:
|
||||
return sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _get_npm_downloads(package: Package) -> int:
|
||||
"""Retrieve the weekly download count for a package on the npm registry."""
|
||||
|
||||
@@ -82,14 +86,18 @@ def _get_npm_downloads(package: Package) -> int:
|
||||
npm_response = requests.get(npm_url)
|
||||
npm_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
raise AssertionError(f"Package {package['name']} does not exist on npm registry")
|
||||
raise AssertionError(
|
||||
f"Package {package['name']} does not exist on npm registry"
|
||||
)
|
||||
|
||||
npm_data = npm_response.json()
|
||||
|
||||
# Retrieve the first publish date using the 'created' timestamp from the 'time' field.
|
||||
created_str = npm_data.get("time", {}).get("created")
|
||||
if created_str is None:
|
||||
raise AssertionError(f"Package {package['name']} has no creation time in registry data")
|
||||
raise AssertionError(
|
||||
f"Package {package['name']} has no creation time in registry data"
|
||||
)
|
||||
# Remove the trailing 'Z' if present and parse the ISO format timestamp
|
||||
first_publish_date = datetime.fromisoformat(created_str.rstrip("Z"))
|
||||
|
||||
@@ -103,7 +111,10 @@ def _get_npm_downloads(package: Package) -> int:
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> list[ResolvedPackage]:
|
||||
|
||||
def _get_weekly_downloads(
|
||||
packages: dict[str, list[Package]], fake: bool
|
||||
) -> list[ResolvedPackage]:
|
||||
"""Retrieve the weekly download count for a dictionary of python or js packages."""
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
@@ -131,7 +142,7 @@ def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> lis
|
||||
num_downloads = _get_npm_downloads(package)
|
||||
else:
|
||||
num_downloads = None
|
||||
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
@@ -145,12 +156,13 @@ def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> lis
|
||||
|
||||
return resolved_packages
|
||||
|
||||
|
||||
def main(output_file: str, fake: bool) -> None:
|
||||
"""Main function to generate package download information.
|
||||
|
||||
Args:
|
||||
output_file: Path to the output YAML file.
|
||||
fake: If True, use fake download counts for testing purposes.
|
||||
fake: If `True`, use fake download counts for testing purposes.
|
||||
"""
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ LangGraph provides three ways to manage context, which combines the mutability a
|
||||
|
||||
**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution.
|
||||
|
||||
!!! version-added "New in LangGraph v0.6: `context` replaces `config['configurable']`"
|
||||
!!! version-added "Added in version 0.6.0: `context` replaces `config['configurable']`"
|
||||
|
||||
Runtime context is now passed to the `context` argument of `invoke`/`stream`,
|
||||
which replaces the previous pattern of passing application configuration to `config['configurable']`.
|
||||
@@ -90,7 +90,7 @@ graph.invoke( # (1)!
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
# highlight-next-line
|
||||
def node(state: State, config: Runtime[ContextSchema]):
|
||||
def node(state: State, runtime: Runtime[ContextSchema]):
|
||||
user_name = runtime.context.user_name
|
||||
...
|
||||
```
|
||||
|
||||
@@ -211,7 +211,7 @@ output = agent.invoke(
|
||||
print(output["messages"][-1].text())
|
||||
```
|
||||
|
||||
!!! version-added "New in LangGraph v0.6"
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
|
||||
:::
|
||||
|
||||
@@ -351,11 +351,13 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
||||
:::python
|
||||
|
||||
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://python.langchain.com/docs/how_to/custom_chat_model/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://js.langchain.com/docs/how_to/custom_chat/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
||||
|
||||
:::
|
||||
|
||||
2. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](../how-tos/streaming.md#use-with-any-llm) with `StreamWriter`.
|
||||
@@ -371,6 +373,7 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
||||
- [Force model to call a specific tool](https://python.langchain.com/docs/how_to/tool_choice/)
|
||||
- [All chat model how-to guides](https://python.langchain.com/docs/how_to/#chat-models)
|
||||
- [Chat model integrations](https://python.langchain.com/docs/integrations/chat/)
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
@@ -381,4 +384,5 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
||||
- [Force model to call a specific tool](https://js.langchain.com/docs/how_to/tool_choice/)
|
||||
- [All chat model how-to guides](https://js.langchain.com/docs/how_to/#chat-models)
|
||||
- [Chat model integrations](https://js.langchain.com/docs/integrations/chat/)
|
||||
|
||||
:::
|
||||
|
||||
@@ -99,8 +99,8 @@ Starting from the `LangGraph Platform` view...
|
||||
1. In the top-right corner, select the gear icon (`Deployment Settings`).
|
||||
1. Update the `Git Branch` to the desired branch.
|
||||
1. Check/uncheck checkbox to `Automatically update deployment on push to branch`.
|
||||
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
|
||||
1. Pushes in quick succession to a branch will not trigger subsequent updates. In the future, this functionality may be changed/improved.
|
||||
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
|
||||
1. Pushes in quick succession to a branch will queue subsequent updates. Once a build completes, the most recent commit will begin building and the other queued builds will be skipped.
|
||||
|
||||
## Add or Remove GitHub Repositories
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "LangGraph Platform",
|
||||
"title": "LangSmith Deployment",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tags": [
|
||||
@@ -29,6 +29,10 @@
|
||||
"name": "Store",
|
||||
"description": "Store is an API for managing persistent key-value store (long-term memory) that is available from any thread."
|
||||
},
|
||||
{
|
||||
"name": "A2A",
|
||||
"description": "Agent-to-Agent Protocol related endpoints for exposing assistants as A2A-compliant agents."
|
||||
},
|
||||
{
|
||||
"name": "MCP",
|
||||
"description": "Model Context Protocol related endpoints for exposing an agent as an MCP server."
|
||||
@@ -1520,6 +1524,96 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/threads/{thread_id}/stream": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Threads"
|
||||
],
|
||||
"summary": "Join Thread Stream",
|
||||
"description": "This endpoint streams output in real-time from a thread. The stream will include the output of each run executed sequentially on the thread and will remain open indefinitely. It is the responsibility of the calling client to close the connection.",
|
||||
"operationId": "join_thread_stream_threads__thread_id__stream_get",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The ID of the thread.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Thread Id",
|
||||
"description": "The ID of the thread."
|
||||
},
|
||||
"name": "thread_id",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Last Event ID",
|
||||
"description": "The ID of the last event received. Used to resume streaming from a specific point. Pass '-' to resume from the beginning."
|
||||
},
|
||||
"name": "Last-Event-ID",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["lifecycle", "run_modes", "state_update"]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["lifecycle", "run_modes", "state_update"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"default": ["run_modes"],
|
||||
"title": "Stream Modes",
|
||||
"description": "Stream modes to control which events are returned. 'lifecycle' returns only run start/end events, 'run_modes' returns all run events (default behavior), 'state_update' returns only state update events."
|
||||
},
|
||||
"name": "stream_modes",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/event-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/threads/{thread_id}/runs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -3092,6 +3186,195 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/a2a/{assistant_id}": {
|
||||
"post": {
|
||||
"operationId": "post_a2a",
|
||||
"summary": "A2A Post",
|
||||
"description": "Communicate with an assistant using the Agent-to-Agent Protocol.\nSends a JSON-RPC 2.0 message to the assistant.\n\n- **Request**: Provide an object with `jsonrpc`, `id`, `method`, and optional `params`.\n- **Response**: Returns a JSON-RPC response with task information or error.\n\n**Supported Methods:**\n- `message/send`: Send a message to the assistant\n- `tasks/get`: Get the status and result of a task\n\n**Notes:**\n- Supports threaded conversations via thread context\n- Messages can contain text and data parts\n- Tasks run asynchronously and return completion status\n",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "assistant_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"description": "The ID of the assistant to communicate with"
|
||||
},
|
||||
{
|
||||
"name": "Accept",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["application/json"]
|
||||
},
|
||||
"description": "Must be application/json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jsonrpc": {
|
||||
"type": "string",
|
||||
"enum": ["2.0"],
|
||||
"description": "JSON-RPC version"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Request identifier"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["message/send", "tasks/get"],
|
||||
"description": "The method to invoke"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Method parameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Message Send Parameters",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": ["user", "assistant"],
|
||||
"description": "Message role"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Text Part",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["text"]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["kind", "text"]
|
||||
},
|
||||
{
|
||||
"title": "Data Part",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["data"]
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["kind", "data"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Message parts"
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string",
|
||||
"description": "Unique message identifier"
|
||||
}
|
||||
},
|
||||
"required": ["role", "parts", "messageId"]
|
||||
},
|
||||
"thread": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string",
|
||||
"description": "Thread identifier for conversation context"
|
||||
}
|
||||
},
|
||||
"description": "Optional thread context"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
},
|
||||
{
|
||||
"title": "Task Get Parameters",
|
||||
"properties": {
|
||||
"taskId": {
|
||||
"type": "string",
|
||||
"description": "Task identifier to retrieve"
|
||||
}
|
||||
},
|
||||
"required": ["taskId"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["jsonrpc", "id", "method"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "JSON-RPC response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jsonrpc": {
|
||||
"type": "string",
|
||||
"enum": ["2.0"]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"description": "Success result containing task information or task details"
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Error information if request failed"
|
||||
}
|
||||
},
|
||||
"required": ["jsonrpc", "id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid JSON-RPC or missing Accept header"
|
||||
},
|
||||
"404": {
|
||||
"description": "Assistant not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"A2A"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/mcp/": {
|
||||
"post": {
|
||||
"operationId": "post_mcp",
|
||||
@@ -4346,6 +4629,17 @@
|
||||
"title": "Checkpoint During",
|
||||
"description": "Whether to checkpoint during the run.",
|
||||
"default": false
|
||||
},
|
||||
"durability": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sync",
|
||||
"async",
|
||||
"exit"
|
||||
],
|
||||
"title": "Durability",
|
||||
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
|
||||
"default": "async"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -4582,6 +4876,17 @@
|
||||
"title": "Checkpoint During",
|
||||
"description": "Whether to checkpoint during the run.",
|
||||
"default": false
|
||||
},
|
||||
"durability": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sync",
|
||||
"async",
|
||||
"exit"
|
||||
],
|
||||
"title": "Durability",
|
||||
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
|
||||
"default": "async"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -4710,6 +5015,12 @@
|
||||
},
|
||||
"ThreadSearchRequest": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "uuid"},
|
||||
"title": "Ids",
|
||||
"description": "List of thread IDs to include. Others are excluded."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"title": "Metadata",
|
||||
@@ -4950,11 +5261,30 @@
|
||||
"type": "object",
|
||||
"title": "Metadata",
|
||||
"description": "Metadata to merge with existing thread metadata."
|
||||
},
|
||||
"ttl": {
|
||||
"type": "object",
|
||||
"title": "TTL",
|
||||
"description": "The time-to-live for the thread.",
|
||||
"properties": {
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"delete"
|
||||
],
|
||||
"description": "The TTL strategy. 'delete' removes the entire thread.",
|
||||
"default": "delete"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "number",
|
||||
"description": "The time-to-live in minutes from now until thread should be swept."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "ThreadPatch",
|
||||
"description": "Payload for creating a thread."
|
||||
"description": "Payload for updating a thread."
|
||||
},
|
||||
"ThreadStateCheckpointRequest": {
|
||||
"properties": {
|
||||
|
||||
@@ -483,19 +483,19 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
|
||||
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
ADD ./graphs /deps/outer-graphs/src
|
||||
RUN set -ex && \
|
||||
for line in '[project]' \
|
||||
'name = "graphs"' \
|
||||
'version = "0.1"' \
|
||||
'[tool.setuptools.package-data]' \
|
||||
'"*" = ["**/*"]'; do \
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
|
||||
echo "$line" >> /deps/outer-graphs/pyproject.toml; \
|
||||
done
|
||||
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-graphs/src/agent.py:graph", "storm": "/deps/outer-graphs/src/storm.py:graph"}'
|
||||
```
|
||||
|
||||
???+ note "Updating your langgraph.json file"
|
||||
|
||||
@@ -21,12 +21,16 @@ 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.
|
||||
|
||||
:::python
|
||||
:::python
|
||||
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][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).
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
:::js
|
||||
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][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
|
||||
@@ -61,7 +65,7 @@ LangGraph supports three durability modes that allow you to balance performance
|
||||
|
||||
A higher durability mode add more overhead to the workflow execution.
|
||||
|
||||
!!! version-added "Added in v0.6.0"
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
|
||||
Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management:
|
||||
|
||||
@@ -73,14 +77,16 @@ A higher durability mode add more overhead to the workflow execution.
|
||||
* `checkpoint_during=True` -> `durability="async"`
|
||||
* `checkpoint_during=False` -> `durability="exit"`
|
||||
|
||||
|
||||
### `"exit"`
|
||||
|
||||
Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution.
|
||||
|
||||
### `"async"`
|
||||
|
||||
Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution.
|
||||
|
||||
### `"sync"`
|
||||
|
||||
Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead.
|
||||
|
||||
You can specify the durability mode when calling any graph execution method:
|
||||
@@ -310,12 +316,14 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[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](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[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 `null` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
:::
|
||||
|
||||
## Starting Points for Resuming Workflows
|
||||
@@ -326,6 +334,7 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
||||
- 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.
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
@@ -334,4 +343,5 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
||||
- 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.
|
||||
|
||||
:::
|
||||
|
||||
@@ -1040,7 +1040,7 @@ def node_a(state: State, runtime: Runtime[ContextSchema]):
|
||||
...
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
|
||||
See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full breakdown on configuration.
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
@@ -134,7 +134,7 @@ def update_instructions(state: State, store: BaseStore):
|
||||
namespace = ("instructions",)
|
||||
current_instructions = store.search(namespace)[0]
|
||||
# Memory logic
|
||||
prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
|
||||
prompt = prompt_template.format(instructions=current_instructions.value["instructions"], conversation=state["messages"])
|
||||
output = llm.invoke(prompt)
|
||||
new_instructions = output['new_instructions']
|
||||
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
|
||||
@@ -278,4 +278,4 @@ const items = await store.search(
|
||||
```
|
||||
:::
|
||||
|
||||
For more information about the memory store, see the [Persistence](persistence.md#memory-store) guide.
|
||||
For more information about the memory store, see the [Persistence](persistence.md#memory-store) guide.
|
||||
|
||||
@@ -897,5 +897,5 @@ There are two high-level approaches to achieve that:
|
||||
|
||||
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
|
||||
|
||||
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
||||
- Define agent node functions with a [private input state schema](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.md#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
||||
- Define agent node functions with a [private input state schema](../how-tos/graph-api.md#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||
|
||||
@@ -1019,7 +1019,7 @@ console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "b" } }));
|
||||
# Usage
|
||||
input_message = {"role": "user", "content": "hi"}
|
||||
# With no configuration, uses default (Anthropic)
|
||||
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
|
||||
response_1 = graph.invoke({"messages": [input_message]}, context=ContextSchema())["messages"][-1]
|
||||
# Or, can set OpenAI
|
||||
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
|
||||
|
||||
@@ -1205,7 +1205,7 @@ There are many use cases where you may wish for your node to have a custom retry
|
||||
To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node:
|
||||
|
||||
```python
|
||||
from langgraph.pregel import RetryPolicy
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
builder.add_node(
|
||||
"node_name",
|
||||
@@ -1260,7 +1260,7 @@ By default, the retry policy retries on any exception except for the following:
|
||||
from typing_extensions import TypedDict
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langgraph.graph import END, MessagesState, StateGraph, START
|
||||
from langgraph.pregel import RetryPolicy
|
||||
from langgraph.types import RetryPolicy
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
@@ -1422,15 +1422,15 @@ const builder = new StateGraph(State)
|
||||
:::
|
||||
|
||||
??? info "Why split application steps into a sequence with LangGraph?"
|
||||
LangGraph makes it easy to add an underlying persistence layer to your application.
|
||||
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
|
||||
LangGraph makes it easy to add an underlying persistence layer to your application.
|
||||
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
|
||||
|
||||
- How state updates are [checkpointed](../concepts/persistence.md)
|
||||
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
|
||||
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
|
||||
- How state updates are [checkpointed](../concepts/persistence.md)
|
||||
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
|
||||
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
|
||||
|
||||
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
|
||||
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
|
||||
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
|
||||
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
|
||||
|
||||
Let's demonstrate an end-to-end example. We will create a sequence of three steps:
|
||||
|
||||
@@ -2110,7 +2110,6 @@ builder.add_edge(START, "generate_topics")
|
||||
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
|
||||
builder.add_edge("generate_joke", "best_joke")
|
||||
builder.add_edge("best_joke", END)
|
||||
builder.add_edge("generate_topics", END)
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
@@ -2333,7 +2332,7 @@ from IPython.display import Image, display
|
||||
display(Image(graph.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||

|
||||
:::
|
||||
|
||||
:::js
|
||||
@@ -3272,7 +3271,7 @@ from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeSt
|
||||
display(Image(app.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
**Using Mermaid + Pyppeteer**
|
||||
|
||||
@@ -3320,4 +3319,4 @@ const imageBuffer = new Uint8Array(await image.arrayBuffer());
|
||||
|
||||
await fs.writeFile("graph.png", imageBuffer);
|
||||
```
|
||||
:::
|
||||
:::
|
||||
|
||||
@@ -366,8 +366,8 @@ result = graph.invoke(
|
||||
|
||||
# Resume with mapping of interrupt IDs to values
|
||||
resume_map = {
|
||||
i.interrupt_id: f"human input for prompt {i.value}"
|
||||
for i in parent.get_state(thread_config).interrupts
|
||||
i.id: f"edited text for {i.value['text_to_revise']}"
|
||||
for i in graph.get_state(config).interrupts
|
||||
}
|
||||
print(graph.invoke(Command(resume=resume_map), config=config))
|
||||
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
|
||||
|
||||
@@ -244,7 +244,7 @@ output = agent.invoke(
|
||||
print(output["messages"][-1].text())
|
||||
```
|
||||
|
||||
!!! version-added "New in langgraph>=0.6"
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
::: langgraph.cache.base
|
||||
::: langgraph.cache.memory
|
||||
::: langgraph.cache.sqlite
|
||||
::: langgraph.cache.sqlite
|
||||
|
||||
@@ -68,7 +68,7 @@ The server will start and open the studio in your browser:
|
||||
> - 📚 API Docs: http://127.0.0.1:2024/docs
|
||||
>
|
||||
> This in-memory server is designed for development and testing.
|
||||
> For production use, please use LangGraph Platform.
|
||||
> For production use, please use LangSmith Deployment.
|
||||
```
|
||||
|
||||
If you were to self-host this on the public internet, anyone could access it!
|
||||
|
||||
@@ -294,9 +294,9 @@ Now that you have a LangGraph app running locally, take your journey further by
|
||||
:::python
|
||||
|
||||
- [Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md): Explore the Python SDK API Reference.
|
||||
:::
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
- [JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md): Explore the JS/TS SDK API Reference.
|
||||
:::
|
||||
:::
|
||||
|
||||
@@ -1948,7 +1948,7 @@ const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
|
||||
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
|
||||
def should_continue(state: MessagesState) -> Literal["environment", END]:
|
||||
def should_continue(state: MessagesState) -> Literal["Action", END]:
|
||||
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
|
||||
|
||||
messages = state["messages"]
|
||||
|
||||
+62
-98
@@ -149,6 +149,67 @@ plugins:
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- agents/agents.md
|
||||
- concepts/why-langgraph.md
|
||||
- tutorials/get-started/1-build-basic-chatbot.md
|
||||
- tutorials/get-started/2-add-tools.md
|
||||
- tutorials/get-started/3-add-memory.md
|
||||
- tutorials/get-started/4-human-in-the-loop.md
|
||||
- tutorials/get-started/5-customize-state.md
|
||||
- tutorials/get-started/6-time-travel.md
|
||||
- tutorials/langgraph-platform/local-server.md
|
||||
- tutorials/workflows.md
|
||||
- concepts/agentic_concepts.md
|
||||
- guides/index.md
|
||||
- agents/overview.md
|
||||
- agents/run_agents.md
|
||||
- concepts/low_level.md
|
||||
- how-tos/graph-api.md
|
||||
- concepts/functional_api.md
|
||||
- how-tos/use-functional-api.md
|
||||
- concepts/pregel.md
|
||||
- concepts/streaming.md
|
||||
- how-tos/streaming.md
|
||||
- concepts/persistence.md
|
||||
- concepts/durable_execution.md
|
||||
- concepts/memory.md
|
||||
- how-tos/memory/add-memory.md
|
||||
- agents/context.md
|
||||
- agents/models.md
|
||||
- concepts/tools.md
|
||||
- how-tos/tool-calling.md
|
||||
- concepts/human_in_the_loop.md
|
||||
- how-tos/human_in_the_loop/add-human-in-the-loop.md
|
||||
- concepts/time-travel.md
|
||||
- how-tos/human_in_the_loop/time-travel.md
|
||||
- concepts/subgraphs.md
|
||||
- how-tos/subgraph.md
|
||||
- concepts/multi_agent.md
|
||||
- agents/multi-agent.md
|
||||
- how-tos/multi_agent.md
|
||||
- concepts/mcp.md
|
||||
- agents/mcp.md
|
||||
- concepts/tracing.md
|
||||
- how-tos/enable-tracing.md
|
||||
- agents/evals.md
|
||||
- examples/index.md
|
||||
- concepts/template_applications.md # TODO: make tutorial
|
||||
- tutorials/rag/langgraph_agentic_rag.md
|
||||
- tutorials/multi_agent/agent_supervisor.md
|
||||
- tutorials/sql/sql-agent.md
|
||||
- agents/ui.md
|
||||
- how-tos/run-id-langsmith.md
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- adopters.md
|
||||
- concepts/faq.md
|
||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
|
||||
- tags
|
||||
- include-markdown
|
||||
- mkdocstrings:
|
||||
@@ -186,75 +247,6 @@ plugins:
|
||||
- "!^_"
|
||||
|
||||
nav:
|
||||
- Get started:
|
||||
- index.md
|
||||
- Quickstarts:
|
||||
- Start with a prebuilt agent: agents/agents.md
|
||||
- Build a custom workflow:
|
||||
- concepts/why-langgraph.md
|
||||
- 1. Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
|
||||
- 2. Add tools: tutorials/get-started/2-add-tools.md
|
||||
- 3. Add memory: tutorials/get-started/3-add-memory.md
|
||||
- 4. Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
|
||||
- 5. Customize state: tutorials/get-started/5-customize-state.md
|
||||
- 6. Time travel: tutorials/get-started/6-time-travel.md
|
||||
- Run a local server: tutorials/langgraph-platform/local-server.md
|
||||
- General concepts:
|
||||
- Workflows & agents: tutorials/workflows.md
|
||||
- Agent architectures: concepts/agentic_concepts.md
|
||||
|
||||
- Guides:
|
||||
- guides/index.md
|
||||
- Agent development:
|
||||
- Overview: agents/overview.md
|
||||
- Run an agent: agents/run_agents.md
|
||||
- LangGraph APIs:
|
||||
- Graph API:
|
||||
- Overview: concepts/low_level.md
|
||||
- Use the Graph API: how-tos/graph-api.md
|
||||
- Functional API:
|
||||
- Overview: concepts/functional_api.md
|
||||
- Use the Functional API: how-tos/use-functional-api.md
|
||||
- Runtime: concepts/pregel.md
|
||||
- Core capabilities:
|
||||
- Streaming:
|
||||
- Overview: concepts/streaming.md
|
||||
- Stream outputs: how-tos/streaming.md
|
||||
- Persistence:
|
||||
- Overview: concepts/persistence.md
|
||||
- Durable execution:
|
||||
- Overview: concepts/durable_execution.md
|
||||
- Memory:
|
||||
- Overview: concepts/memory.md
|
||||
- Add memory: how-tos/memory/add-memory.md
|
||||
- Context:
|
||||
- Add context: agents/context.md
|
||||
- Models:
|
||||
- Configure model: agents/models.md
|
||||
- Tools:
|
||||
- Overview: concepts/tools.md
|
||||
- Call tools: how-tos/tool-calling.md
|
||||
- Human-in-the-loop:
|
||||
- Overview: concepts/human_in_the_loop.md
|
||||
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
|
||||
- Time travel:
|
||||
- Overview: concepts/time-travel.md
|
||||
- Use time travel: how-tos/human_in_the_loop/time-travel.md
|
||||
- Subgraphs:
|
||||
- Overview: concepts/subgraphs.md
|
||||
- Use subgraphs: how-tos/subgraph.md
|
||||
- Multi-agent:
|
||||
- Overview: concepts/multi_agent.md
|
||||
- Prebuilt implementation: agents/multi-agent.md
|
||||
- Custom implementation: how-tos/multi_agent.md
|
||||
- MCP:
|
||||
- Overview: concepts/mcp.md
|
||||
- Use MCP: agents/mcp.md
|
||||
- Tracing:
|
||||
- Overview: concepts/tracing.md
|
||||
- Enable tracing: how-tos/enable-tracing.md
|
||||
- Evaluate performance: agents/evals.md
|
||||
|
||||
- Reference:
|
||||
- reference/index.md
|
||||
- LangGraph:
|
||||
@@ -278,35 +270,7 @@ nav:
|
||||
- LangGraph Platform:
|
||||
- SDK (Python): cloud/reference/sdk/python_sdk_ref.md
|
||||
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
|
||||
- Examples:
|
||||
- examples/index.md
|
||||
- Template applications: concepts/template_applications.md # TODO: make tutorial
|
||||
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.md
|
||||
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.md
|
||||
- SQL agent: tutorials/sql/sql-agent.md
|
||||
- Prebuilt chat UI: agents/ui.md
|
||||
- Graph runs in LangSmith: how-tos/run-id-langsmith.md
|
||||
|
||||
- Additional resources:
|
||||
- additional-resources/index.md
|
||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- LangGraph Academy course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||
- Case studies: adopters.md
|
||||
- concepts/faq.md
|
||||
- llms.txt: llms-txt-overview.md
|
||||
- LangChain Forum: https://forum.langchain.com/
|
||||
- Troubleshooting:
|
||||
- Errors:
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
|
||||
markdown_extensions:
|
||||
- abbr
|
||||
|
||||
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
}
|
||||
|
||||
.md-banner {
|
||||
background-color: #CFC9FA;
|
||||
background-color: #FFAE42;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
{% endblock %}
|
||||
|
||||
{% block announce %}
|
||||
Our new LangChain Academy Course Deep Research with LangGraph is now live! <a href="https://academy.langchain.com/courses/deep-research-with-langgraph/?utm_medium=internal&utm_source=docs&utm_campaign=q3-2025_deep-research-course_co" target="_blank">Enroll for free</a>.
|
||||
These docs will be deprecated and removed with the release of LangGraph v1.0 in October 2025. <a href="https://docs.langchain.com/oss/python/langgraph/overview" target="_blank">Visit the v1.0 alpha docs</a>
|
||||
{% endblock %}
|
||||
|
||||
+4
-4
@@ -7,14 +7,14 @@ name = "langgraph-docs"
|
||||
version = "0.0.1"
|
||||
description = "LangGraph docs"
|
||||
authors = []
|
||||
requires-python = "~=3.11"
|
||||
requires-python = ">=3.11.0,<4.0.0"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = [
|
||||
"aiohappyeyeballs==2.4.3",
|
||||
"hub>=3.0.1,<4",
|
||||
"xxhash>=3.5.0,<4",
|
||||
"black>=25.1.0,<26",
|
||||
"hub>=3.0.1,<4.0.0",
|
||||
"xxhash>=3.5.0,<4.0.0",
|
||||
"black>=25.1.0,<26.0.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
Generated
+5
-4
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.11, <4"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
|
||||
@@ -2337,7 +2337,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.6.2"
|
||||
version = "0.6.7"
|
||||
source = { editable = "../libs/langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2380,6 +2380,7 @@ dev = [
|
||||
{ name = "pytest-repeat" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "pytest-xdist", extras = ["psutil"] },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "types-requests" },
|
||||
@@ -2413,6 +2414,7 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -2643,7 +2645,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.2"
|
||||
version = "0.6.4"
|
||||
source = { editable = "../libs/prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2674,7 +2676,6 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.2.0"
|
||||
source = { editable = "../libs/sdk-py" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "18526f23",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -707,7 +707,9 @@
|
||||
" \"\"\"\n",
|
||||
" Find all tool calls in the messages returned\n",
|
||||
" \"\"\"\n",
|
||||
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
|
||||
" tool_calls = [\n",
|
||||
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
|
||||
" ]\n",
|
||||
" return tool_calls\n",
|
||||
"\n",
|
||||
"\n",
|
||||
|
||||
@@ -7,11 +7,6 @@ from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
@@ -19,12 +14,17 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
get_serializable_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
Conn = _internal.Conn # For backward compatibility
|
||||
|
||||
@@ -94,6 +94,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
strict=False,
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
@@ -115,12 +116,12 @@ class PostgresSaver(BasePostgresSaver):
|
||||
|
||||
Args:
|
||||
config: The config to use for listing the checkpoints.
|
||||
filter: Additional filtering criteria for metadata. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit: The maximum number of checkpoints to return. Defaults to None.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: The maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
||||
An iterator of checkpoint tuples.
|
||||
|
||||
Examples:
|
||||
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
||||
@@ -182,7 +183,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -190,7 +191,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -325,7 +326,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(copy),
|
||||
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -450,7 +451,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": {
|
||||
**value["checkpoint"].get("channel_values"),
|
||||
**(value["checkpoint"].get("channel_values") or {}),
|
||||
**self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Union
|
||||
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import DictRow
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
|
||||
Conn = AsyncConnection[DictRow] | AsyncConnectionPool[AsyncConnection[DictRow]]
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import Union
|
||||
|
||||
from psycopg import Connection
|
||||
from psycopg.rows import DictRow
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
|
||||
Conn = Connection[DictRow] | ConnectionPool[Connection[DictRow]]
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -7,11 +7,6 @@ from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
@@ -19,12 +14,17 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
get_serializable_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
Conn = _ainternal.Conn # For backward compatibility
|
||||
|
||||
@@ -99,6 +99,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
strict=False,
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
@@ -121,11 +122,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
Args:
|
||||
config: Base configuration for filtering checkpoints.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||
An asynchronous iterator of matching checkpoint tuples.
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
@@ -169,7 +170,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -177,7 +178,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_id = get_checkpoint_id(config)
|
||||
@@ -283,7 +284,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(copy),
|
||||
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -409,7 +410,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": {
|
||||
**value["checkpoint"].get("channel_values"),
|
||||
**(value["checkpoint"].get("channel_values") or {}),
|
||||
**self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
},
|
||||
@@ -444,11 +445,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
Args:
|
||||
config: Base configuration for filtering checkpoints.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
try:
|
||||
# check if we are in the main thread, only bg threads can block
|
||||
@@ -476,7 +477,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -484,7 +485,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
try:
|
||||
# check if we are in the main thread, only bg threads can block
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Optional, cast
|
||||
from importlib.metadata import version as get_version
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -14,8 +14,21 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_id,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
MetadataInput = Optional[dict[str, Any]]
|
||||
MetadataInput = dict[str, Any] | None
|
||||
|
||||
try:
|
||||
major, minor = get_version("langgraph").split(".")[:2]
|
||||
if int(major) == 0 and int(minor) < 5:
|
||||
warnings.warn(
|
||||
"You're using incompatible versions of langgraph and checkpoint-postgres. Please upgrade langgraph to avoid unexpected behavior.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
except Exception:
|
||||
# skip version check if running from source
|
||||
pass
|
||||
|
||||
"""
|
||||
To add a new migration, add a new string to the MIGRATIONS list.
|
||||
|
||||
@@ -3,9 +3,19 @@ import threading
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_serializable_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import (
|
||||
AsyncConnection,
|
||||
AsyncCursor,
|
||||
@@ -19,18 +29,8 @@ from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import _ainternal, _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
|
||||
"""
|
||||
To add a new migration, add a new string to the MIGRATIONS list.
|
||||
@@ -151,7 +151,7 @@ def _dump_blobs(
|
||||
checkpoint_ns: str,
|
||||
values: dict[str, Any],
|
||||
versions: ChannelVersions,
|
||||
) -> list[tuple[str, str, str, str, Optional[bytes]]]:
|
||||
) -> list[tuple[str, str, str, str, bytes | None]]:
|
||||
if not versions:
|
||||
return []
|
||||
|
||||
@@ -186,8 +186,8 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
def __init__(
|
||||
self,
|
||||
conn: _internal.Conn,
|
||||
pipe: Optional[Pipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
pipe: Pipeline | None = None,
|
||||
serde: SerializerProtocol | None = None,
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
@@ -249,6 +249,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
strict=False,
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
@@ -257,11 +258,11 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
config: RunnableConfig | None,
|
||||
*,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
|
||||
@@ -299,7 +300,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
pending_writes=self._load_writes(value["pending_writes"]),
|
||||
)
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
@@ -309,7 +310,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -441,7 +442,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(copy),
|
||||
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -542,8 +543,8 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
def __init__(
|
||||
self,
|
||||
conn: _ainternal.Conn,
|
||||
pipe: Optional[AsyncPipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
pipe: AsyncPipeline | None = None,
|
||||
serde: SerializerProtocol | None = None,
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
@@ -570,7 +571,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
conn_string: str,
|
||||
*,
|
||||
pipeline: bool = False,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
serde: SerializerProtocol | None = None,
|
||||
) -> AsyncIterator["AsyncShallowPostgresSaver"]:
|
||||
"""Create a new AsyncShallowPostgresSaver instance from a connection string.
|
||||
|
||||
@@ -610,6 +611,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
strict=False,
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
@@ -618,11 +620,11 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
|
||||
async def alist(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
config: RunnableConfig | None,
|
||||
*,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> AsyncIterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database asynchronously.
|
||||
|
||||
@@ -662,7 +664,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
),
|
||||
)
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
@@ -672,7 +674,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
@@ -774,7 +776,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(copy),
|
||||
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -861,11 +863,11 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
config: RunnableConfig | None,
|
||||
*,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
|
||||
@@ -883,7 +885,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
@@ -893,7 +895,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
try:
|
||||
# check if we are in the main thread, only bg threads can block
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from langgraph.store.postgres.aio import AsyncPostgresStore
|
||||
from langgraph.store.postgres.base import PostgresStore
|
||||
from langgraph.store.postgres.base import PoolConfig, PostgresStore
|
||||
|
||||
__all__ = ["AsyncPostgresStore", "PostgresStore"]
|
||||
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
|
||||
|
||||
@@ -2,17 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import orjson
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
ListNamespacesOp,
|
||||
@@ -22,6 +17,11 @@ from langgraph.store.base import (
|
||||
SearchOp,
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.store.postgres.base import (
|
||||
PLACEHOLDER,
|
||||
BasePostgresStore,
|
||||
@@ -339,7 +339,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the task to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
If `None`, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the task was successfully stopped or wasn't running,
|
||||
@@ -465,7 +465,9 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
query,
|
||||
[
|
||||
p
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
|
||||
for (ns, k, pathname, _), vector in zip(
|
||||
txt_params, vectors, strict=False
|
||||
)
|
||||
for p in (ns, k, pathname, vector)
|
||||
],
|
||||
)
|
||||
@@ -486,13 +488,13 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
vectors = await self.embeddings.aembed_documents(
|
||||
[query for _, query in embedding_requests]
|
||||
)
|
||||
for (idx, _), vector in zip(embedding_requests, vectors):
|
||||
for (idx, _), vector in zip(embedding_requests, vectors, strict=False):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = vector
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
|
||||
await cur.execute(query, params)
|
||||
rows = cast(list[Row], await cur.fetchall())
|
||||
items = [
|
||||
@@ -510,7 +512,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
cur: AsyncCursor[DictRow],
|
||||
) -> None:
|
||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
||||
await cur.execute(query, params)
|
||||
rows = cast(list[dict], await cur.fetchall())
|
||||
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
||||
|
||||
@@ -6,30 +6,20 @@ import json
|
||||
import logging
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
import orjson
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
||||
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
@@ -46,6 +36,14 @@ from langgraph.store.base import (
|
||||
get_text_at_path,
|
||||
tokenize_path,
|
||||
)
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
||||
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.embeddings import Embeddings
|
||||
@@ -141,7 +139,7 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS store_vectors_embedding_idx ON store_vec
|
||||
]
|
||||
|
||||
|
||||
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
|
||||
C = TypeVar("C", bound=_pg_internal.Conn | _ainternal.Conn)
|
||||
|
||||
|
||||
class PoolConfig(TypedDict, total=False):
|
||||
@@ -255,7 +253,7 @@ class BasePostgresStore(Generic[C]):
|
||||
|
||||
results = []
|
||||
for namespace, items in namespace_groups.items():
|
||||
_, keys = zip(*items)
|
||||
_, keys = zip(*items, strict=False)
|
||||
this_refresh_ttls = refresh_ttls[namespace]
|
||||
|
||||
query = """
|
||||
@@ -868,7 +866,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the thread to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
If `None`, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the thread was successfully stopped or wasn't running,
|
||||
@@ -1014,7 +1012,9 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
query,
|
||||
[
|
||||
p
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
|
||||
for (ns, k, pathname, _), vector in zip(
|
||||
txt_params, vectors, strict=False
|
||||
)
|
||||
for p in (ns, k, pathname, vector)
|
||||
],
|
||||
)
|
||||
@@ -1035,13 +1035,15 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
embeddings = self.embeddings.embed_documents(
|
||||
[query for _, query in embedding_requests]
|
||||
)
|
||||
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
for (idx, _), embedding in zip(
|
||||
embedding_requests, embeddings, strict=False
|
||||
):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = embedding
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
|
||||
cur.execute(query, params)
|
||||
rows = cast(list[Row], cur.fetchall())
|
||||
results[idx] = [
|
||||
@@ -1058,7 +1060,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
cur: Cursor[DictRow],
|
||||
) -> None:
|
||||
for (query, params), (idx, _) in zip(
|
||||
self._get_batch_list_namespaces_queries(list_ops), list_ops
|
||||
self._get_batch_list_namespaces_queries(list_ops), list_ops, strict=False
|
||||
):
|
||||
cur.execute(query, params)
|
||||
results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
|
||||
|
||||
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.23"
|
||||
version = "2.0.25"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
dependencies = [
|
||||
"langgraph-checkpoint>=2.0.21,<3.0.0",
|
||||
"langgraph-checkpoint>=2.1.2,<3.0.0",
|
||||
"orjson>=3.10.1",
|
||||
"psycopg>=3.2.0",
|
||||
"psycopg-pool>=3.2.0",
|
||||
@@ -55,8 +55,10 @@ lint.select = [
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
lint.ignore = ["E501", "B008"]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.mypy]
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
|
||||
@@ -6,10 +6,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
@@ -17,11 +13,15 @@ from langgraph.checkpoint.base import (
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
@@ -187,13 +187,11 @@ def test_data():
|
||||
metadata_1: CheckpointMetadata = {
|
||||
"source": "input",
|
||||
"step": 2,
|
||||
"writes": {},
|
||||
"score": 1,
|
||||
}
|
||||
metadata_2: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
metadata_3: CheckpointMetadata = {}
|
||||
@@ -220,7 +218,6 @@ async def test_combined_metadata(saver_name: str, test_data) -> None:
|
||||
metadata: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
await saver.aput(config, chkpnt, metadata, {})
|
||||
@@ -246,7 +243,6 @@ async def test_asearch(saver_name: str, test_data) -> None:
|
||||
query_1 = {"source": "input"} # search by 1 key
|
||||
query_2 = {
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
} # search by multiple keys
|
||||
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
||||
query_4 = {"source": "update", "step": 1} # no match
|
||||
@@ -344,3 +340,34 @@ async def test_pending_sends_migration(saver_name: str) -> None:
|
||||
TASKS: ["send-1", "send-2", "send-3"]
|
||||
}
|
||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
||||
async def test_get_checkpoint_no_channel_values(
|
||||
monkeypatch, saver_name: str, test_data
|
||||
) -> None:
|
||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
||||
async with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
await saver.aput(config, chkpnt, {}, {})
|
||||
|
||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
||||
|
||||
def patched_load_checkpoint_tuple(value):
|
||||
value["checkpoint"].pop("channel_values", None)
|
||||
return load_checkpoint_tuple(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
||||
)
|
||||
|
||||
checkpoint = await saver.aget_tuple(config)
|
||||
assert checkpoint.checkpoint["channel_values"] == {}
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import sys
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -12,8 +11,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -21,6 +18,8 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from langgraph.store.postgres import AsyncPostgresStore
|
||||
from tests.conftest import (
|
||||
DEFAULT_URI,
|
||||
@@ -34,9 +33,6 @@ TTL_MINUTES = TTL_SECONDS / 60
|
||||
|
||||
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
|
||||
async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
|
||||
database = f"test_{uuid.uuid4().hex[:16]}"
|
||||
uri_parts = DEFAULT_URI.split("/")
|
||||
uri_base = "/".join(uri_parts[:-1])
|
||||
@@ -358,8 +354,6 @@ async def _create_vector_store(
|
||||
text_fields: list[str] | None = None,
|
||||
) -> AsyncIterator[AsyncPostgresStore]:
|
||||
"""Create a store with vector search enabled."""
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
|
||||
database = f"test_{uuid.uuid4().hex[:16]}"
|
||||
uri_parts = DEFAULT_URI.split("/")
|
||||
|
||||
@@ -9,8 +9,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from psycopg import Connection
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -19,6 +17,8 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
from psycopg import Connection
|
||||
|
||||
from langgraph.store.postgres import PostgresStore
|
||||
from tests.conftest import (
|
||||
DEFAULT_URI,
|
||||
@@ -754,7 +754,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
|
||||
similarities = []
|
||||
for y in Y:
|
||||
dot_product = sum(a * b for a, b in zip(X, y))
|
||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
||||
norm1 = sum(a * a for a in X) ** 0.5
|
||||
norm2 = sum(a * a for a in y) ** 0.5
|
||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||
@@ -771,7 +771,7 @@ def _inner_product(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
|
||||
similarities = []
|
||||
for y in Y:
|
||||
similarity = sum(a * b for a, b in zip(X, y))
|
||||
similarity = sum(a * b for a, b in zip(X, y, strict=False))
|
||||
similarities.append(similarity)
|
||||
|
||||
return similarities
|
||||
@@ -785,7 +785,7 @@ def _neg_l2_distance(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
|
||||
similarities = []
|
||||
for y in Y:
|
||||
similarity = sum((a - b) ** 2 for a, b in zip(X, y)) ** 0.5
|
||||
similarity = sum((a - b) ** 2 for a, b in zip(X, y, strict=False)) ** 0.5
|
||||
similarities.append(-similarity)
|
||||
|
||||
return similarities
|
||||
@@ -861,3 +861,41 @@ def test_store_ttl(store):
|
||||
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
|
||||
res = store.search(ns, query="bar", refresh_ttl=False)
|
||||
assert len(res) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vector_type,distance_type",
|
||||
[
|
||||
("vector", "cosine"),
|
||||
("vector", "inner_product"),
|
||||
("halfvec", "cosine"),
|
||||
("halfvec", "inner_product"),
|
||||
],
|
||||
)
|
||||
def test_non_ascii(
|
||||
request: Any,
|
||||
fake_embeddings: CharacterEmbeddings,
|
||||
vector_type: str,
|
||||
distance_type: str,
|
||||
) -> None:
|
||||
"""Test support for non-ascii characters"""
|
||||
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
|
||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
||||
store.put(
|
||||
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
||||
) # Japanese
|
||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
||||
|
||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
||||
|
||||
assert result1[0].key == "1"
|
||||
assert result2[0].key == "2"
|
||||
assert result3[0].key == "3"
|
||||
assert result4[0].key == "4"
|
||||
assert result5[0].key == "5"
|
||||
|
||||
@@ -7,10 +7,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Connection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
@@ -18,8 +14,12 @@ from langgraph.checkpoint.base import (
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import Connection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
@@ -169,13 +169,11 @@ def test_data():
|
||||
metadata_1: CheckpointMetadata = {
|
||||
"source": "input",
|
||||
"step": 2,
|
||||
"writes": {},
|
||||
"score": 1,
|
||||
}
|
||||
metadata_2: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
metadata_3: CheckpointMetadata = {}
|
||||
@@ -202,7 +200,6 @@ def test_combined_metadata(saver_name: str, test_data) -> None:
|
||||
metadata: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
saver.put(config, chkpnt, metadata, {})
|
||||
@@ -228,7 +225,6 @@ def test_search(saver_name: str, test_data) -> None:
|
||||
query_1 = {"source": "input"} # search by 1 key
|
||||
query_2 = {
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
} # search by multiple keys
|
||||
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
||||
query_4 = {"source": "update", "step": 1} # no match
|
||||
@@ -332,3 +328,33 @@ def test_pending_sends_migration(saver_name: str) -> None:
|
||||
TASKS: ["send-1", "send-2", "send-3"]
|
||||
}
|
||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
||||
def test_get_checkpoint_no_channel_values(
|
||||
monkeypatch, saver_name: str, test_data
|
||||
) -> None:
|
||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
||||
with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
saver.put(config, chkpnt, {}, {})
|
||||
|
||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
||||
|
||||
def patched_load_checkpoint_tuple(value):
|
||||
value["checkpoint"].pop("channel_values", None)
|
||||
return load_checkpoint_tuple(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
||||
)
|
||||
|
||||
checkpoint = saver.get_tuple(config)
|
||||
assert checkpoint.checkpoint["channel_values"] == {}
|
||||
|
||||
Generated
+420
-534
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ from contextlib import closing, contextmanager
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -21,6 +20,7 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
from langgraph.checkpoint.sqlite.utils import search_where
|
||||
|
||||
_AIO_ERROR_MSG = (
|
||||
@@ -184,7 +184,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -192,7 +192,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -301,12 +301,12 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
|
||||
Args:
|
||||
config: The config to use for listing the checkpoints.
|
||||
filter: Additional filtering criteria for metadata. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit: The maximum number of checkpoints to return. Defaults to None.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: The maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
||||
An iterator of checkpoint tuples.
|
||||
|
||||
Examples:
|
||||
>>> from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
|
||||
@@ -2,13 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, Callable, TypeVar, cast
|
||||
from typing import Any, TypeVar, cast
|
||||
|
||||
import aiosqlite
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -21,6 +20,7 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
from langgraph.checkpoint.sqlite.utils import search_where
|
||||
|
||||
T = TypeVar("T", bound=Callable)
|
||||
@@ -139,7 +139,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -147,7 +147,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
try:
|
||||
# check if we are in the main thread, only bg threads can block
|
||||
@@ -181,11 +181,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
Args:
|
||||
config: Base configuration for filtering checkpoints.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
try:
|
||||
# check if we are in the main thread, only bg threads can block
|
||||
@@ -316,7 +316,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -324,7 +324,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
await self.setup()
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
@@ -414,11 +414,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
Args:
|
||||
config: Base configuration for filtering checkpoints.
|
||||
filter: Additional filtering criteria for metadata.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||
An asynchronous iterator of matching checkpoint tuples.
|
||||
"""
|
||||
await self.setup()
|
||||
where, params = search_where(config, filter, before)
|
||||
|
||||
@@ -5,7 +5,6 @@ from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import get_checkpoint_id
|
||||
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import aiosqlite
|
||||
import orjson
|
||||
import sqlite_vec # type: ignore[import-untyped]
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
ListNamespacesOp,
|
||||
@@ -22,6 +21,7 @@ from langgraph.store.base import (
|
||||
TTLConfig,
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
|
||||
from langgraph.store.sqlite.base import (
|
||||
_PLACEHOLDER,
|
||||
BaseSqliteStore,
|
||||
@@ -303,7 +303,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the task to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
If `None`, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the task was successfully stopped or wasn't running,
|
||||
@@ -484,7 +484,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
|
||||
# Convert vectors to SQLite-friendly format
|
||||
vector_params = []
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
||||
vector_params.extend(
|
||||
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
||||
)
|
||||
@@ -507,7 +507,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
results: List to store results in.
|
||||
cur: Database cursor.
|
||||
"""
|
||||
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
||||
search_ops
|
||||
)
|
||||
|
||||
# Setup dot_product function if it doesn't exist
|
||||
if embedding_requests and self.embeddings:
|
||||
@@ -515,23 +517,62 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
[query for _, query in embedding_requests]
|
||||
)
|
||||
|
||||
for (idx, _), embedding in zip(embedding_requests, vectors):
|
||||
_params_list: list = queries[idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
for (embed_req_idx, _), embedding in zip(
|
||||
embedding_requests, vectors, strict=False
|
||||
):
|
||||
# Find the corresponding query in prepared_queries
|
||||
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries
|
||||
if embed_req_idx < len(prepared_queries):
|
||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
||||
)
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
||||
search_ops, prepared_queries, strict=False
|
||||
):
|
||||
await cur.execute(query, params)
|
||||
rows = await cur.fetchall()
|
||||
|
||||
if "score" in query:
|
||||
if needs_refresh and rows and self.ttl_config:
|
||||
keys_to_refresh = []
|
||||
for row_data in rows:
|
||||
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
|
||||
# These are raw text values directly from the DB.
|
||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
||||
|
||||
if keys_to_refresh:
|
||||
updates_by_prefix = defaultdict(list)
|
||||
for prefix_text, key_text in keys_to_refresh:
|
||||
updates_by_prefix[prefix_text].append(key_text)
|
||||
|
||||
for prefix_text, key_list in updates_by_prefix.items():
|
||||
placeholders = ",".join(["?"] * len(key_list))
|
||||
update_query = f"""
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
||||
"""
|
||||
update_params = (prefix_text, *key_list)
|
||||
try:
|
||||
await cur.execute(update_query, update_params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error during TTL refresh update for search: {e}"
|
||||
)
|
||||
|
||||
# Process rows into items
|
||||
if "score" in query: # Vector search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
_decode_ns_text(row[0]),
|
||||
_decode_ns_text(row[0]), # prefix
|
||||
{
|
||||
"key": row[1],
|
||||
"value": row[2],
|
||||
"key": row[1], # key
|
||||
"value": row[2], # value
|
||||
"created_at": row[3],
|
||||
"updated_at": row[4],
|
||||
"expires_at": row[5] if len(row) > 5 else None,
|
||||
@@ -545,10 +586,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
else: # Regular search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
_decode_ns_text(row[0]),
|
||||
_decode_ns_text(row[0]), # prefix
|
||||
{
|
||||
"key": row[1],
|
||||
"value": row[2],
|
||||
"key": row[1], # key
|
||||
"value": row[2], # value
|
||||
"created_at": row[3],
|
||||
"updated_at": row[4],
|
||||
"expires_at": row[5] if len(row) > 5 else None,
|
||||
@@ -559,7 +600,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
for row in rows
|
||||
]
|
||||
|
||||
results[idx] = items
|
||||
results[original_op_idx] = items
|
||||
|
||||
async def _batch_list_namespaces_ops(
|
||||
self,
|
||||
@@ -575,7 +616,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
cur: Database cursor.
|
||||
"""
|
||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
||||
await cur.execute(query, params)
|
||||
|
||||
rows = await cur.fetchall()
|
||||
|
||||
@@ -7,13 +7,12 @@ import re
|
||||
import sqlite3
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Callable, Literal, NamedTuple, cast
|
||||
from typing import Any, Literal, NamedTuple, cast
|
||||
|
||||
import orjson
|
||||
import sqlite_vec # type: ignore[import-untyped]
|
||||
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
@@ -233,7 +232,7 @@ class BaseSqliteStore:
|
||||
|
||||
results = []
|
||||
for namespace, items in namespace_groups.items():
|
||||
_, keys = zip(*items)
|
||||
_, keys = zip(*items, strict=False)
|
||||
this_refresh_ttls = refresh_ttls[namespace]
|
||||
refresh_ttl_any = any(this_refresh_ttls)
|
||||
|
||||
@@ -372,13 +371,15 @@ class BaseSqliteStore:
|
||||
def _prepare_batch_search_queries(
|
||||
self, search_ops: Sequence[tuple[int, SearchOp]]
|
||||
) -> tuple[
|
||||
list[tuple[str, list[None | str | list[float]]]], # queries, params
|
||||
list[
|
||||
tuple[str, list[None | str | list[float]], bool]
|
||||
], # queries, params, needs_refresh
|
||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||
]:
|
||||
"""
|
||||
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
||||
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
|
||||
Returns:
|
||||
- queries: list of (SQL, param_list)
|
||||
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
|
||||
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
||||
"""
|
||||
queries = []
|
||||
@@ -519,30 +520,18 @@ class BaseSqliteStore:
|
||||
logger.debug(f"Search query: {base_query}")
|
||||
logger.debug(f"Search params: {params}")
|
||||
|
||||
# Handle TTL refresh if requested
|
||||
if (
|
||||
# Determine if TTL refresh is needed
|
||||
needs_ttl_refresh = bool(
|
||||
op.refresh_ttl
|
||||
and self.ttl_config
|
||||
and self.ttl_config.get("refresh_on_read", False)
|
||||
):
|
||||
final_sql = f"""
|
||||
WITH search_results AS (
|
||||
{base_query}
|
||||
),
|
||||
updated AS (
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
|
||||
AND ttl_minutes IS NOT NULL
|
||||
)
|
||||
SELECT * FROM search_results
|
||||
"""
|
||||
final_params = params[:] # copy params
|
||||
else:
|
||||
final_sql = base_query
|
||||
final_params = params
|
||||
)
|
||||
|
||||
queries.append((final_sql, final_params))
|
||||
# The base_query is now the final_sql, and we pass the refresh flag
|
||||
final_sql = base_query
|
||||
final_params = params
|
||||
|
||||
queries.append((final_sql, final_params, needs_ttl_refresh))
|
||||
|
||||
return queries, embedding_requests
|
||||
|
||||
@@ -840,7 +829,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
|
||||
results = []
|
||||
for namespace, items in namespace_groups.items():
|
||||
_, keys = zip(*items)
|
||||
_, keys = zip(*items, strict=False)
|
||||
this_refresh_ttls = refresh_ttls[namespace]
|
||||
refresh_ttl_any = any(this_refresh_ttls)
|
||||
|
||||
@@ -1167,7 +1156,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the thread to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
If `None`, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the thread was successfully stopped or wasn't running,
|
||||
@@ -1315,7 +1304,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
|
||||
# Convert vectors to SQLite-friendly format
|
||||
vector_params = []
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
||||
vector_params.extend(
|
||||
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
||||
)
|
||||
@@ -1331,7 +1320,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
results: list[Result],
|
||||
cur: sqlite3.Cursor,
|
||||
) -> None:
|
||||
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
||||
search_ops
|
||||
)
|
||||
|
||||
# Setup similarity functions if they don't exist
|
||||
if embedding_requests and self.embeddings:
|
||||
@@ -1341,16 +1332,50 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
)
|
||||
|
||||
# Replace placeholders with actual embeddings
|
||||
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
_params_list: list = queries[idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
for (embed_req_idx, _), embedding in zip(
|
||||
embedding_requests, embeddings, strict=False
|
||||
):
|
||||
if embed_req_idx < len(prepared_queries):
|
||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
||||
)
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
||||
search_ops, prepared_queries, strict=False
|
||||
):
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if needs_refresh and rows and self.ttl_config:
|
||||
keys_to_refresh = []
|
||||
for row_data in rows:
|
||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
||||
|
||||
if keys_to_refresh:
|
||||
updates_by_prefix = defaultdict(list)
|
||||
for prefix_text, key_text in keys_to_refresh:
|
||||
updates_by_prefix[prefix_text].append(key_text)
|
||||
|
||||
for prefix_text, key_list in updates_by_prefix.items():
|
||||
placeholders = ",".join(["?"] * len(key_list))
|
||||
update_query = f"""
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
||||
"""
|
||||
update_params = (prefix_text, *key_list)
|
||||
try:
|
||||
cur.execute(update_query, update_params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error during TTL refresh update for search: {e}"
|
||||
)
|
||||
|
||||
if "score" in query: # Vector search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
@@ -1385,7 +1410,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
for row in rows
|
||||
]
|
||||
|
||||
results[idx] = items
|
||||
results[original_op_idx] = items
|
||||
|
||||
def _batch_list_namespaces_ops(
|
||||
self,
|
||||
@@ -1394,7 +1419,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
cur: sqlite3.Cursor,
|
||||
) -> None:
|
||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
||||
cur.execute(query, params)
|
||||
results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.11"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
@@ -53,8 +53,10 @@ lint.select = [
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
lint.ignore = ["E501", "B008"]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.pytest-watcher]
|
||||
now = true
|
||||
|
||||
@@ -2,13 +2,13 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ import tempfile
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator, Generator, Iterable
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional, Union, cast
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -16,6 +15,7 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
from langgraph.store.sqlite import AsyncSqliteStore
|
||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||
from tests.test_store import CharacterEmbeddings
|
||||
@@ -51,7 +51,7 @@ def fake_embeddings() -> CharacterEmbeddings:
|
||||
async def create_vector_store(
|
||||
fake_embeddings: CharacterEmbeddings,
|
||||
conn_string: str = ":memory:",
|
||||
text_fields: Optional[list[str]] = None,
|
||||
text_fields: list[str] | None = None,
|
||||
) -> AsyncIterator[AsyncSqliteStore]:
|
||||
"""Create an AsyncSqliteStore with vector search capabilities."""
|
||||
index_config: SqliteIndexConfig = {
|
||||
@@ -168,7 +168,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
|
||||
]
|
||||
|
||||
results = await store.abatch(
|
||||
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
||||
)
|
||||
assert len(results) == 5
|
||||
assert isinstance(results[0], Item)
|
||||
@@ -193,7 +193,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
|
||||
]
|
||||
|
||||
results_reordered = await store.abatch(
|
||||
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
|
||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
|
||||
)
|
||||
assert len(results_reordered) == 5
|
||||
assert isinstance(results_reordered[0], list)
|
||||
@@ -681,7 +681,7 @@ async def test_search_items(
|
||||
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
||||
) as store:
|
||||
# Insert test data
|
||||
for ns, item in zip(test_namespaces, test_items):
|
||||
for ns, item in zip(test_namespaces, test_items, strict=False):
|
||||
key = f"item_{ns[-1]}"
|
||||
await store.aput(ns, key, item)
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
|
||||
|
||||
@@ -116,7 +116,17 @@ class TestSqliteSaver:
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
# search with before param
|
||||
search_results_6 = list(saver.list(None, before=search_results_5[1].config))
|
||||
assert len(search_results_6) == 1
|
||||
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
|
||||
|
||||
# search with limit param
|
||||
search_results_7 = list(
|
||||
saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
|
||||
)
|
||||
assert len(search_results_7) == 1
|
||||
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
|
||||
|
||||
def test_search_where(self) -> None:
|
||||
# call method / assertions
|
||||
|
||||
@@ -5,11 +5,10 @@ import tempfile
|
||||
import uuid
|
||||
from collections.abc import Generator, Iterable
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Literal, Optional, Union, cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -18,6 +17,7 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
from langgraph.store.sqlite import SqliteStore
|
||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||
|
||||
@@ -110,7 +110,7 @@ VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
|
||||
@contextmanager
|
||||
def create_vector_store(
|
||||
fake_embeddings: CharacterEmbeddings,
|
||||
text_fields: Optional[list[str]] = None,
|
||||
text_fields: list[str] | None = None,
|
||||
distance_type: str = "cosine",
|
||||
conn_type: Literal["memory", "file"] = "memory",
|
||||
) -> Generator[SqliteStore, None, None]:
|
||||
@@ -153,7 +153,7 @@ def test_batch_order(store: SqliteStore) -> None:
|
||||
]
|
||||
|
||||
results = store.batch(
|
||||
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
||||
)
|
||||
assert len(results) == 5
|
||||
assert isinstance(results[0], Item)
|
||||
@@ -182,7 +182,7 @@ def test_batch_order(store: SqliteStore) -> None:
|
||||
]
|
||||
|
||||
results_reordered = store.batch(
|
||||
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
|
||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
|
||||
)
|
||||
assert len(results_reordered) == 5
|
||||
assert isinstance(results_reordered[0], list)
|
||||
@@ -301,7 +301,7 @@ def test_batch_list_namespaces_ops(store: SqliteStore) -> None:
|
||||
]
|
||||
|
||||
results = store.batch(
|
||||
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
||||
)
|
||||
assert len(results) == 3
|
||||
|
||||
@@ -778,7 +778,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
|
||||
similarities = []
|
||||
for y in Y:
|
||||
dot_product = sum(a * b for a, b in zip(X, y))
|
||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
||||
norm1 = sum(a * a for a in X) ** 0.5
|
||||
norm2 = sum(a * a for a in y) ** 0.5
|
||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||
@@ -1011,7 +1011,7 @@ def test_search_items(
|
||||
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
||||
) as store:
|
||||
# Insert test data
|
||||
for ns, item in zip(test_namespaces, test_items):
|
||||
for ns, item in zip(test_namespaces, test_items, strict=False):
|
||||
key = f"item_{ns[-1]}"
|
||||
store.put(ns, key, item)
|
||||
|
||||
@@ -1067,3 +1067,31 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid filter key"):
|
||||
store.search(("docs",), filter={malicious_key: "dummy"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
||||
def test_non_ascii(
|
||||
fake_embeddings: CharacterEmbeddings,
|
||||
distance_type: str,
|
||||
) -> None:
|
||||
"""Test support for non-ascii characters"""
|
||||
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
||||
store.put(
|
||||
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
||||
) # Japanese
|
||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
||||
|
||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
||||
|
||||
assert result1[0].key == "1"
|
||||
assert result2[0].key == "2"
|
||||
assert result3[0].key == "3"
|
||||
assert result4[0].key == "4"
|
||||
assert result5[0].key == "5"
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
from langgraph.store.base import TTLConfig
|
||||
|
||||
from langgraph.store.sqlite import SqliteStore
|
||||
from langgraph.store.sqlite.aio import AsyncSqliteStore
|
||||
@@ -93,9 +94,13 @@ def test_ttl_sweeper(temp_db_file: str) -> None:
|
||||
ttl_seconds = 2
|
||||
ttl_minutes = ttl_seconds / 60
|
||||
|
||||
ttl_config: TTLConfig = {
|
||||
"default_ttl": ttl_minutes,
|
||||
"sweep_interval_minutes": ttl_minutes / 2,
|
||||
}
|
||||
with SqliteStore.from_conn_string(
|
||||
temp_db_file,
|
||||
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
store.setup()
|
||||
|
||||
@@ -298,9 +303,14 @@ async def test_async_ttl_sweeper(temp_db_file: str) -> None:
|
||||
ttl_seconds = 2
|
||||
ttl_minutes = ttl_seconds / 60
|
||||
|
||||
ttl_config: TTLConfig = {
|
||||
"default_ttl": ttl_minutes,
|
||||
"sweep_interval_minutes": ttl_minutes / 2,
|
||||
}
|
||||
|
||||
async with AsyncSqliteStore.from_conn_string(
|
||||
temp_db_file,
|
||||
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
await store.setup()
|
||||
|
||||
@@ -353,3 +363,67 @@ async def test_async_search_with_ttl(temp_db_file: str) -> None:
|
||||
# Search after expiration
|
||||
results = await store.asearch(("test",), filter={"value": "apple"})
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.flaky(retries=3)
|
||||
async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
|
||||
"""Test TTL refresh on asearch with async API."""
|
||||
ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
|
||||
ttl_minutes = ttl_seconds / 60.0
|
||||
|
||||
async with AsyncSqliteStore.from_conn_string(
|
||||
temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
|
||||
) as store:
|
||||
await store.setup()
|
||||
|
||||
namespace = ("docs", "user1")
|
||||
# t=0: items put, expire at t=4.0s
|
||||
await store.aput(namespace, "item1", {"text": "content1", "id": 1})
|
||||
await store.aput(namespace, "item2", {"text": "content2", "id": 2})
|
||||
|
||||
# t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
|
||||
await asyncio.sleep(ttl_seconds * 0.75)
|
||||
|
||||
# Perform asearch with refresh_ttl=True for item1.
|
||||
# item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
|
||||
# item2's TTL is not affected. Expires at t=4.0s.
|
||||
searched_items = await store.asearch(
|
||||
namespace, filter={"id": 1}, refresh_ttl=True
|
||||
)
|
||||
assert len(searched_items) == 1
|
||||
assert searched_items[0].key == "item1"
|
||||
|
||||
# t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
|
||||
await asyncio.sleep(ttl_seconds * 0.5)
|
||||
# At this point:
|
||||
# - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
|
||||
# - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
|
||||
|
||||
await store.sweep_ttl()
|
||||
|
||||
# Check item1 (should exist due to asearch refresh)
|
||||
item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
|
||||
assert item1_check1 is not None, (
|
||||
"Item1 should exist after asearch refresh and first sweep"
|
||||
)
|
||||
assert item1_check1.value["text"] == "content1"
|
||||
|
||||
# Check item2 (should be gone)
|
||||
item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
|
||||
assert item2_check1 is None, (
|
||||
"Item2 should be gone after its original TTL expired"
|
||||
)
|
||||
|
||||
# t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
|
||||
await asyncio.sleep(ttl_seconds * 0.625)
|
||||
# At this point:
|
||||
# - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
|
||||
|
||||
await store.sweep_ttl()
|
||||
|
||||
# Check item1 again (should be gone now)
|
||||
item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
|
||||
assert item1_final_check is None, (
|
||||
"Item1 should be gone after its refreshed TTL expired"
|
||||
)
|
||||
|
||||
Generated
+370
-474
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ from typing import ( # noqa: UP035
|
||||
NamedTuple,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
@@ -35,17 +34,17 @@ class CheckpointMetadata(TypedDict, total=False):
|
||||
source: Literal["input", "loop", "update", "fork"]
|
||||
"""The source of the checkpoint.
|
||||
|
||||
- "input": The checkpoint was created from an input to invoke/stream/batch.
|
||||
- "loop": The checkpoint was created from inside the pregel loop.
|
||||
- "update": The checkpoint was created from a manual state update.
|
||||
- "fork": The checkpoint was created as a copy of another checkpoint.
|
||||
- `"input"`: The checkpoint was created from an input to invoke/stream/batch.
|
||||
- `"loop"`: The checkpoint was created from inside the pregel loop.
|
||||
- `"update"`: The checkpoint was created from a manual state update.
|
||||
- `"fork"`: The checkpoint was created as a copy of another checkpoint.
|
||||
"""
|
||||
step: int
|
||||
"""The step number of the checkpoint.
|
||||
|
||||
-1 for the first "input" checkpoint.
|
||||
0 for the first "loop" checkpoint.
|
||||
... for the nth checkpoint afterwards.
|
||||
`-1` for the first `"input"` checkpoint.
|
||||
`0` for the first `"loop"` checkpoint.
|
||||
`...` for the `nth` checkpoint afterwards.
|
||||
"""
|
||||
parents: dict[str, str]
|
||||
"""The IDs of the parent checkpoints.
|
||||
@@ -54,7 +53,7 @@ class CheckpointMetadata(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
ChannelVersions = dict[str, Union[str, int, float]]
|
||||
ChannelVersions = dict[str, str | int | float]
|
||||
|
||||
|
||||
class Checkpoint(TypedDict):
|
||||
@@ -148,7 +147,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
config: Configuration specifying which checkpoint to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[Checkpoint]: The requested checkpoint, or None if not found.
|
||||
The requested checkpoint, or `None` if not found.
|
||||
"""
|
||||
if value := self.get_tuple(config):
|
||||
return value.checkpoint
|
||||
@@ -160,7 +159,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
config: Configuration specifying which checkpoint to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
|
||||
The requested checkpoint tuple, or `None` if not found.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||
@@ -184,7 +183,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Returns:
|
||||
Iterator[CheckpointTuple]: Iterator of matching checkpoint tuples.
|
||||
Iterator of matching checkpoint tuples.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||
@@ -252,7 +251,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
config: Configuration specifying which checkpoint to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[Checkpoint]: The requested checkpoint, or None if not found.
|
||||
The requested checkpoint, or `None` if not found.
|
||||
"""
|
||||
if value := await self.aget_tuple(config):
|
||||
return value.checkpoint
|
||||
@@ -264,7 +263,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
config: Configuration specifying which checkpoint to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
|
||||
The requested checkpoint tuple, or `None` if not found.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||
@@ -288,7 +287,7 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Returns:
|
||||
AsyncIterator[CheckpointTuple]: Async iterator of matching checkpoint tuples.
|
||||
Async iterator of matching checkpoint tuples.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||
@@ -404,6 +403,16 @@ def get_checkpoint_metadata(
|
||||
return metadata
|
||||
|
||||
|
||||
def get_serializable_checkpoint_metadata(
|
||||
config: RunnableConfig, metadata: CheckpointMetadata
|
||||
) -> CheckpointMetadata:
|
||||
"""Get checkpoint metadata in a backwards-compatible manner."""
|
||||
checkpoint_metadata = get_checkpoint_metadata(config, metadata)
|
||||
if "writes" in checkpoint_metadata:
|
||||
checkpoint_metadata.pop("writes")
|
||||
return checkpoint_metadata
|
||||
|
||||
|
||||
"""
|
||||
Mapping from error type to error index.
|
||||
Regular writes just map to their index in the list of writes being saved.
|
||||
|
||||
@@ -39,7 +39,7 @@ class InMemorySaver(
|
||||
Only use `InMemorySaver` for debugging or testing purposes.
|
||||
For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
|
||||
|
||||
If you are using the LangGraph Platform, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
|
||||
If you are using LangSmith Deployment, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
|
||||
|
||||
Args:
|
||||
serde: The serializer to use for serializing and deserializing checkpoints. Defaults to None.
|
||||
@@ -133,7 +133,7 @@ class InMemorySaver(
|
||||
"""Get a checkpoint tuple from the in-memory storage.
|
||||
|
||||
This method retrieves a checkpoint tuple from the in-memory storage based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
@@ -141,7 +141,7 @@ class InMemorySaver(
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
thread_id: str = config["configurable"]["thread_id"]
|
||||
checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
|
||||
@@ -231,7 +231,7 @@ class InMemorySaver(
|
||||
limit: Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
|
||||
config_checkpoint_ns = (
|
||||
@@ -423,16 +423,16 @@ class InMemorySaver(
|
||||
del self.blobs[k]
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||
"""Asynchronous version of get_tuple.
|
||||
"""Asynchronous version of `get_tuple`.
|
||||
|
||||
This method is an asynchronous wrapper around get_tuple that runs the synchronous
|
||||
This method is an asynchronous wrapper around `get_tuple` that runs the synchronous
|
||||
method in a separate thread using asyncio.
|
||||
|
||||
Args:
|
||||
config: The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return self.get_tuple(config)
|
||||
|
||||
@@ -444,16 +444,16 @@ class InMemorySaver(
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> AsyncIterator[CheckpointTuple]:
|
||||
"""Asynchronous version of list.
|
||||
"""Asynchronous version of `list`.
|
||||
|
||||
This method is an asynchronous wrapper around list that runs the synchronous
|
||||
This method is an asynchronous wrapper around `list` that runs the synchronous
|
||||
method in a separate thread using asyncio.
|
||||
|
||||
Args:
|
||||
config: The config to use for listing the checkpoints.
|
||||
|
||||
Yields:
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
|
||||
An asynchronous iterator of checkpoint tuples.
|
||||
"""
|
||||
for item in self.list(config, filter=filter, before=before, limit=limit):
|
||||
yield item
|
||||
@@ -465,7 +465,7 @@ class InMemorySaver(
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Asynchronous version of put.
|
||||
"""Asynchronous version of `put`.
|
||||
|
||||
Args:
|
||||
config: The config to associate with the checkpoint.
|
||||
@@ -485,9 +485,9 @@ class InMemorySaver(
|
||||
task_id: str,
|
||||
task_path: str = "",
|
||||
) -> None:
|
||||
"""Asynchronous version of put_writes.
|
||||
"""Asynchronous version of `put_writes`.
|
||||
|
||||
This method is an asynchronous wrapper around put_writes that runs the synchronous
|
||||
This method is an asynchronous wrapper around `put_writes` that runs the synchronous
|
||||
method in a separate thread using asyncio.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -9,7 +9,7 @@ import pickle
|
||||
import re
|
||||
import sys
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from inspect import isclass
|
||||
@@ -21,7 +21,7 @@ from ipaddress import (
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Any, cast
|
||||
from uuid import UUID
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
runtime_checkable,
|
||||
@@ -28,9 +27,9 @@ class ChannelProtocol(Protocol[Value, Update, C]):
|
||||
@property
|
||||
def UpdateType(self) -> Any: ...
|
||||
|
||||
def checkpoint(self) -> Optional[C]: ...
|
||||
def checkpoint(self) -> C | None: ...
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[C]) -> Self: ...
|
||||
def from_checkpoint(self, checkpoint: C | None) -> Self: ...
|
||||
|
||||
def update(self, values: Sequence[Update]) -> bool: ...
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from typing import (
|
||||
Literal,
|
||||
NamedTuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
@@ -57,7 +56,7 @@ class Item:
|
||||
key: Unique identifier within the namespace.
|
||||
namespace: Hierarchical path defining the collection in which this document resides.
|
||||
Represented as a tuple of strings, allowing for nested categorization.
|
||||
For example: ("documents", 'user123')
|
||||
For example: `("documents", 'user123')`
|
||||
created_at: Timestamp of item creation.
|
||||
updated_at: Timestamp of last update.
|
||||
"""
|
||||
@@ -249,12 +248,12 @@ class SearchOp(NamedTuple):
|
||||
The filter supports both exact matches and operator-based comparisons.
|
||||
|
||||
Supported Operators:
|
||||
- $eq: Equal to (same as direct value comparison)
|
||||
- $ne: Not equal to
|
||||
- $gt: Greater than
|
||||
- $gte: Greater than or equal to
|
||||
- $lt: Less than
|
||||
- $lte: Less than or equal to
|
||||
- `$eq`: Equal to (same as direct value comparison)
|
||||
- `$ne`: Not equal to
|
||||
- `$gt`: Greater than
|
||||
- `$gte`: Greater than or equal to
|
||||
- `$lt`: Less than
|
||||
- `$lte`: Less than or equal to
|
||||
|
||||
???+ example "Examples"
|
||||
Simple exact match:
|
||||
@@ -302,7 +301,7 @@ class SearchOp(NamedTuple):
|
||||
|
||||
|
||||
# Type representing a namespace path that can include wildcards
|
||||
NamespacePath = tuple[Union[str, Literal["*"]], ...]
|
||||
NamespacePath = tuple[str | Literal["*"], ...]
|
||||
"""A tuple representing a namespace path that can include wildcards.
|
||||
|
||||
???+ example "Examples"
|
||||
@@ -480,12 +479,12 @@ class PutOp(NamedTuple):
|
||||
vector similarity search (if supported by the store implementation).
|
||||
|
||||
Path Syntax:
|
||||
- Simple field access: "field"
|
||||
- Nested fields: "parent.child.grandchild"
|
||||
- Simple field access: `"field"`
|
||||
- Nested fields: `"parent.child.grandchild"`
|
||||
- Array indexing:
|
||||
- Specific index: "array[0]"
|
||||
- Last element: "array[-1]"
|
||||
- All elements (each individually): "array[*]"
|
||||
- Specific index: `"array[0]"`
|
||||
- Last element: `"array[-1]"`
|
||||
- All elements (each individually): `"array[*]"`
|
||||
|
||||
???+ example "Examples"
|
||||
- None - Use store defaults (whole item)
|
||||
@@ -509,12 +508,12 @@ class PutOp(NamedTuple):
|
||||
will expire this many minutes after it was last accessed. The expiration timer
|
||||
refreshes on both read operations (get/search) and write operations (put/update).
|
||||
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
|
||||
Defaults to None (no expiration).
|
||||
Defaults to `None` (no expiration).
|
||||
"""
|
||||
|
||||
|
||||
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
|
||||
Result = Union[Item, list[Item], list[SearchItem], list[tuple[str, ...]], None]
|
||||
Op = GetOp | SearchOp | PutOp | ListNamespacesOp
|
||||
Result = Item | list[Item] | list[SearchItem] | list[tuple[str, ...]] | None
|
||||
|
||||
|
||||
class InvalidNamespaceError(ValueError):
|
||||
@@ -525,18 +524,18 @@ class TTLConfig(TypedDict, total=False):
|
||||
"""Configuration for TTL (time-to-live) behavior in the store."""
|
||||
|
||||
refresh_on_read: bool
|
||||
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
|
||||
"""Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
|
||||
|
||||
If True, TTLs will be refreshed on read operations (get/search) by default.
|
||||
This can be overridden per-operation by explicitly setting refresh_ttl.
|
||||
Defaults to True if not configured.
|
||||
If `True`, TTLs will be refreshed on read operations (get/search) by default.
|
||||
This can be overridden per-operation by explicitly setting `refresh_ttl`.
|
||||
Defaults to `True` if not configured.
|
||||
"""
|
||||
default_ttl: float | None
|
||||
"""Default TTL (time-to-live) in minutes for new items.
|
||||
|
||||
If provided, new items will expire after this many minutes after their last access.
|
||||
The expiration timer refreshes on both read and write operations.
|
||||
Defaults to None (no expiration).
|
||||
Defaults to `None` (no expiration).
|
||||
"""
|
||||
sweep_interval_minutes: int | None
|
||||
"""Interval in minutes between TTL sweep operations.
|
||||
@@ -550,20 +549,20 @@ class IndexConfig(TypedDict, total=False):
|
||||
"""Configuration for indexing documents for semantic search in the store.
|
||||
|
||||
If not provided to the store, the store will not support vector search.
|
||||
In that case, all `index` arguments to put() and `aput()` operations will be ignored.
|
||||
In that case, all `index` arguments to `put()` and `aput()` operations will be ignored.
|
||||
"""
|
||||
|
||||
dims: int
|
||||
"""Number of dimensions in the embedding vectors.
|
||||
|
||||
Common embedding models have the following dimensions:
|
||||
- openai:text-embedding-3-large: 3072
|
||||
- openai:text-embedding-3-small: 1536
|
||||
- openai:text-embedding-ada-002: 1536
|
||||
- cohere:embed-english-v3.0: 1024
|
||||
- cohere:embed-english-light-v3.0: 384
|
||||
- cohere:embed-multilingual-v3.0: 1024
|
||||
- cohere:embed-multilingual-light-v3.0: 384
|
||||
- `openai:text-embedding-3-large`: `3072`
|
||||
- `openai:text-embedding-3-small`: `1536`
|
||||
- `openai:text-embedding-ada-002`: `1536`
|
||||
- `cohere:embed-english-v3.0`: `1024`
|
||||
- `cohere:embed-english-light-v3.0`: `384`
|
||||
- `cohere:embed-multilingual-v3.0`: `1024`
|
||||
- `cohere:embed-multilingual-light-v3.0`: `384`
|
||||
"""
|
||||
|
||||
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
|
||||
@@ -571,12 +570,12 @@ class IndexConfig(TypedDict, total=False):
|
||||
|
||||
Can be specified in three ways:
|
||||
1. A LangChain Embeddings instance
|
||||
2. A synchronous embedding function (EmbeddingsFunc)
|
||||
3. An asynchronous embedding function (AEmbeddingsFunc)
|
||||
4. A provider string (e.g., "openai:text-embedding-3-small")
|
||||
2. A synchronous embedding function (`EmbeddingsFunc`)
|
||||
3. An asynchronous embedding function (`AEmbeddingsFunc`)
|
||||
4. A provider string (e.g., `"openai:text-embedding-3-small"`)
|
||||
|
||||
???+ example "Examples"
|
||||
Using LangChain's initialization with InMemoryStore:
|
||||
Using LangChain's initialization with `InMemoryStore`:
|
||||
```python
|
||||
from langchain.embeddings import init_embeddings
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
@@ -589,7 +588,7 @@ class IndexConfig(TypedDict, total=False):
|
||||
)
|
||||
```
|
||||
|
||||
Using a custom embedding function with InMemoryStore:
|
||||
Using a custom embedding function with `InMemoryStore`:
|
||||
```python
|
||||
from openai import OpenAI
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
@@ -611,7 +610,7 @@ class IndexConfig(TypedDict, total=False):
|
||||
)
|
||||
```
|
||||
|
||||
Using an asynchronous embedding function with InMemoryStore:
|
||||
Using an asynchronous embedding function with `InMemoryStore`:
|
||||
```python
|
||||
from openai import AsyncOpenAI
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
@@ -639,10 +638,10 @@ class IndexConfig(TypedDict, total=False):
|
||||
|
||||
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
|
||||
|
||||
- ["$"]: Embeds the entire JSON object as one vector (default)
|
||||
- ["field1", "field2"]: Embeds specific top-level fields
|
||||
- ["parent.child"]: Embeds nested fields using dot notation
|
||||
- ["array[*].field"]: Embeds field from each array element separately
|
||||
- `["$"]`: Embeds the entire JSON object as one vector (default)
|
||||
- `["field1", "field2"]`: Embeds specific top-level fields
|
||||
- `["parent.child"]`: Embeds nested fields using dot notation
|
||||
- `["array[*].field"]`: Embeds field from each array element separately
|
||||
|
||||
Note:
|
||||
You can always override this behavior when storing an item using the
|
||||
@@ -667,7 +666,7 @@ class IndexConfig(TypedDict, total=False):
|
||||
Note:
|
||||
- Fields missing from a document are skipped
|
||||
- Array notation creates separate embeddings for each element
|
||||
- Complex nested paths are supported (e.g., "a.b[*].c.d")
|
||||
- Complex nested paths are supported (e.g., `"a.b[*].c.d"`)
|
||||
"""
|
||||
|
||||
|
||||
@@ -732,11 +731,11 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned item.
|
||||
If None (default), uses the store's default refresh_ttl setting.
|
||||
If `None`, uses the store's default refresh_ttl setting.
|
||||
If no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
The retrieved item or `None` if not found.
|
||||
"""
|
||||
return self.batch(
|
||||
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
|
||||
@@ -966,7 +965,7 @@ class BaseStore(ABC):
|
||||
key: Unique identifier within the namespace.
|
||||
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
The retrieved item or `None` if not found.
|
||||
"""
|
||||
return (
|
||||
await self.abatch(
|
||||
@@ -1000,7 +999,7 @@ class BaseStore(ABC):
|
||||
limit: Maximum number of items to return.
|
||||
offset: Number of items to skip before returning results.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||
If None (default), uses the store's TTLConfig.refresh_default setting.
|
||||
If `None`, uses the store's TTLConfig.refresh_default setting.
|
||||
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import functools
|
||||
import weakref
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Callable, Literal, TypeVar
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Literal, TypeVar
|
||||
|
||||
from langgraph.store.base import (
|
||||
NOT_PROVIDED,
|
||||
@@ -349,7 +349,7 @@ async def _run(
|
||||
results = [results[ix] for ix in listen]
|
||||
|
||||
# set the results of each operation
|
||||
for fut, result in zip(futs, results):
|
||||
for fut, result in zip(futs, results, strict=False):
|
||||
# guard against future being done (e.g. cancelled)
|
||||
if not fut.done():
|
||||
fut.set_result(result)
|
||||
|
||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
from collections.abc import Awaitable, Sequence
|
||||
from typing import Any, Callable
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
@@ -238,7 +238,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
||||
- Nested paths in multi-field: "{field1,nested.field2}"
|
||||
"""
|
||||
if not path or path == "$":
|
||||
return [json.dumps(obj, sort_keys=True)]
|
||||
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
|
||||
|
||||
tokens = tokenize_path(path) if isinstance(path, str) else path
|
||||
|
||||
@@ -249,7 +249,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
||||
elif obj is None:
|
||||
return []
|
||||
elif isinstance(obj, (list, dict)):
|
||||
return [json.dumps(obj, sort_keys=True)]
|
||||
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
|
||||
return []
|
||||
|
||||
token = tokens[pos]
|
||||
@@ -295,7 +295,11 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
||||
if isinstance(current_obj, (str, int, float, bool)):
|
||||
results.append(str(current_obj))
|
||||
elif isinstance(current_obj, (list, dict)):
|
||||
results.append(json.dumps(current_obj, sort_keys=True))
|
||||
results.append(
|
||||
json.dumps(
|
||||
current_obj, sort_keys=True, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
|
||||
# Handle wildcard
|
||||
elif token == "*":
|
||||
|
||||
@@ -295,7 +295,7 @@ class InMemoryStore(BaseStore):
|
||||
if queries:
|
||||
coros = [self.embeddings.aembed_query(q) for q in list(queries)]
|
||||
results = await asyncio.gather(*coros)
|
||||
queryinmem_store = dict(zip(queries, results))
|
||||
queryinmem_store = dict(zip(queries, results, strict=False))
|
||||
|
||||
return queryinmem_store
|
||||
|
||||
@@ -323,7 +323,9 @@ class InMemoryStore(BaseStore):
|
||||
|
||||
scores = _cosine_similarity(query_embedding, flat_vectors)
|
||||
sorted_results = sorted(
|
||||
zip(scores, flat_items), key=lambda x: x[0], reverse=True
|
||||
zip(scores, flat_items, strict=False),
|
||||
key=lambda x: x[0],
|
||||
reverse=True,
|
||||
)
|
||||
# max pooling
|
||||
seen: set[tuple[tuple[str, ...], str]] = set()
|
||||
@@ -452,7 +454,7 @@ class InMemoryStore(BaseStore):
|
||||
f"Number of embeddings ({len(embeddings)}) does not"
|
||||
f" match number of indices ({len(indices)})"
|
||||
)
|
||||
for embedding, (ns, key, path) in zip(embeddings, indices):
|
||||
for embedding, (ns, key, path) in zip(embeddings, indices, strict=False):
|
||||
self._vectors[ns][key][path] = embedding
|
||||
|
||||
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
||||
@@ -511,7 +513,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
|
||||
similarities = []
|
||||
for y in Y:
|
||||
dot_product = sum(a * b for a, b in zip(X, y))
|
||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
||||
norm1 = sum(a * a for a in X) ** 0.5
|
||||
norm2 = sum(a * a for a in y) ** 0.5
|
||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||
@@ -529,14 +531,14 @@ def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
|
||||
return False
|
||||
|
||||
if match_type == "prefix":
|
||||
for k_elem, p_elem in zip(key, path):
|
||||
for k_elem, p_elem in zip(key, path, strict=False):
|
||||
if p_elem == "*":
|
||||
continue # Wildcard matches any element
|
||||
if k_elem != p_elem:
|
||||
return False
|
||||
return True
|
||||
elif match_type == "suffix":
|
||||
for k_elem, p_elem in zip(reversed(key), reversed(path)):
|
||||
for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):
|
||||
if p_elem == "*":
|
||||
continue # Wildcard matches any element
|
||||
if k_elem != p_elem:
|
||||
@@ -563,7 +565,10 @@ def _compare_values(item_value: Any, filter_value: Any) -> bool:
|
||||
return (
|
||||
isinstance(item_value, (list, tuple))
|
||||
and len(item_value) == len(filter_value)
|
||||
and all(_compare_values(iv, fv) for iv, fv in zip(item_value, filter_value))
|
||||
and all(
|
||||
_compare_values(iv, fv)
|
||||
for iv, fv in zip(item_value, filter_value, strict=False)
|
||||
)
|
||||
)
|
||||
else:
|
||||
return item_value == filter_value
|
||||
|
||||
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
@@ -49,8 +49,10 @@ lint.select = [
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
lint.ignore = ["E501", "B008"]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.pytest-watcher]
|
||||
now = true
|
||||
|
||||
@@ -60,22 +60,15 @@ class MyDataclass:
|
||||
pass
|
||||
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class MyDataclassWSlots:
|
||||
foo: str
|
||||
bar: int
|
||||
inner: InnerDataclass
|
||||
|
||||
class MyDataclassWSlots(MyDataclass):
|
||||
def something(self) -> None:
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class MyDataclassWSlots:
|
||||
foo: str
|
||||
bar: int
|
||||
inner: InnerDataclass
|
||||
|
||||
def something(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class MyEnum(Enum):
|
||||
FOO = "foo"
|
||||
|
||||
@@ -5,12 +5,13 @@ import time
|
||||
import pytest
|
||||
import redis
|
||||
|
||||
from langgraph.cache.base import FullKey
|
||||
from langgraph.cache.redis import RedisCache
|
||||
|
||||
|
||||
class TestRedisCache:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
"""Set up test Redis client and cache."""
|
||||
self.client = redis.Redis(
|
||||
host="localhost", port=6379, db=0, decode_responses=False
|
||||
@@ -20,21 +21,21 @@ class TestRedisCache:
|
||||
except redis.ConnectionError:
|
||||
pytest.skip("Redis server not available")
|
||||
|
||||
self.cache = RedisCache(self.client, prefix="test:cache:")
|
||||
self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:")
|
||||
|
||||
# Clean up before each test
|
||||
self.client.flushdb()
|
||||
|
||||
def teardown_method(self):
|
||||
def teardown_method(self) -> None:
|
||||
"""Clean up after each test."""
|
||||
try:
|
||||
self.client.flushdb()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_basic_set_and_get(self):
|
||||
def test_basic_set_and_get(self) -> None:
|
||||
"""Test basic set and get operations."""
|
||||
keys = [(("graph", "node"), "key1")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "key1")]
|
||||
values = {keys[0]: ({"result": 42}, None)}
|
||||
|
||||
# Set value
|
||||
@@ -45,9 +46,9 @@ class TestRedisCache:
|
||||
assert len(result) == 1
|
||||
assert result[keys[0]] == {"result": 42}
|
||||
|
||||
def test_batch_operations(self):
|
||||
def test_batch_operations(self) -> None:
|
||||
"""Test batch set and get operations."""
|
||||
keys = [
|
||||
keys: list[FullKey] = [
|
||||
(("graph", "node1"), "key1"),
|
||||
(("graph", "node2"), "key2"),
|
||||
(("other", "node"), "key3"),
|
||||
@@ -68,9 +69,9 @@ class TestRedisCache:
|
||||
assert result[keys[1]] == {"result": 2}
|
||||
assert result[keys[2]] == {"result": 3}
|
||||
|
||||
def test_ttl_behavior(self):
|
||||
def test_ttl_behavior(self) -> None:
|
||||
"""Test TTL (time-to-live) functionality."""
|
||||
key = (("graph", "node"), "ttl_key")
|
||||
key: FullKey = (("graph", "node"), "ttl_key")
|
||||
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
|
||||
|
||||
# Set with TTL
|
||||
@@ -88,10 +89,10 @@ class TestRedisCache:
|
||||
result = self.cache.get([key])
|
||||
assert len(result) == 0
|
||||
|
||||
def test_namespace_isolation(self):
|
||||
def test_namespace_isolation(self) -> None:
|
||||
"""Test that different namespaces are isolated."""
|
||||
key1 = (("graph1", "node"), "same_key")
|
||||
key2 = (("graph2", "node"), "same_key")
|
||||
key1: FullKey = (("graph1", "node"), "same_key")
|
||||
key2: FullKey = (("graph2", "node"), "same_key")
|
||||
|
||||
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
|
||||
|
||||
@@ -101,9 +102,12 @@ class TestRedisCache:
|
||||
assert result[key1] == {"graph": 1}
|
||||
assert result[key2] == {"graph": 2}
|
||||
|
||||
def test_clear_all(self):
|
||||
def test_clear_all(self) -> None:
|
||||
"""Test clearing all cached values."""
|
||||
keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
|
||||
keys: list[FullKey] = [
|
||||
(("graph", "node1"), "key1"),
|
||||
(("graph", "node2"), "key2"),
|
||||
]
|
||||
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
|
||||
|
||||
self.cache.set(values)
|
||||
@@ -119,9 +123,9 @@ class TestRedisCache:
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 0
|
||||
|
||||
def test_clear_by_namespace(self):
|
||||
def test_clear_by_namespace(self) -> None:
|
||||
"""Test clearing cached values by namespace."""
|
||||
keys = [
|
||||
keys: list[FullKey] = [
|
||||
(("graph1", "node"), "key1"),
|
||||
(("graph2", "node"), "key2"),
|
||||
(("graph1", "other"), "key3"),
|
||||
@@ -142,7 +146,7 @@ class TestRedisCache:
|
||||
assert len(result) == 1
|
||||
assert result[keys[1]] == {"result": 2}
|
||||
|
||||
def test_empty_operations(self):
|
||||
def test_empty_operations(self) -> None:
|
||||
"""Test behavior with empty keys/values."""
|
||||
# Empty get
|
||||
result = self.cache.get([])
|
||||
@@ -151,14 +155,14 @@ class TestRedisCache:
|
||||
# Empty set
|
||||
self.cache.set({}) # Should not raise error
|
||||
|
||||
def test_nonexistent_keys(self):
|
||||
def test_nonexistent_keys(self) -> None:
|
||||
"""Test getting keys that don't exist."""
|
||||
keys = [(("graph", "node"), "nonexistent")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "nonexistent")]
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_operations(self):
|
||||
async def test_async_operations(self) -> None:
|
||||
"""Test async set and get operations with sync Redis client."""
|
||||
# Create sync Redis client and cache (like main integration tests)
|
||||
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
||||
@@ -167,9 +171,9 @@ class TestRedisCache:
|
||||
except Exception:
|
||||
pytest.skip("Redis not available")
|
||||
|
||||
cache = RedisCache(client, prefix="test:async:")
|
||||
cache: RedisCache = RedisCache(client, prefix="test:async:")
|
||||
|
||||
keys = [(("graph", "node"), "async_key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "async_key")]
|
||||
values = {keys[0]: ({"async": True}, None)}
|
||||
|
||||
# Async set (delegates to sync)
|
||||
@@ -184,7 +188,7 @@ class TestRedisCache:
|
||||
client.flushdb()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_clear(self):
|
||||
async def test_async_clear(self) -> None:
|
||||
"""Test async clear operations with sync Redis client."""
|
||||
# Create sync Redis client and cache (like main integration tests)
|
||||
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
||||
@@ -193,9 +197,9 @@ class TestRedisCache:
|
||||
except Exception:
|
||||
pytest.skip("Redis not available")
|
||||
|
||||
cache = RedisCache(client, prefix="test:async:")
|
||||
cache: RedisCache = RedisCache(client, prefix="test:async:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
await cache.aset(values)
|
||||
@@ -214,44 +218,44 @@ class TestRedisCache:
|
||||
# Cleanup
|
||||
client.flushdb()
|
||||
|
||||
def test_redis_unavailable_get(self):
|
||||
def test_redis_unavailable_get(self) -> None:
|
||||
"""Test behavior when Redis is unavailable during get operations."""
|
||||
# Create cache with non-existent Redis server
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
||||
result = cache.get(keys)
|
||||
|
||||
# Should return empty dict when Redis unavailable
|
||||
assert result == {}
|
||||
|
||||
def test_redis_unavailable_set(self):
|
||||
def test_redis_unavailable_set(self) -> None:
|
||||
"""Test behavior when Redis is unavailable during set operations."""
|
||||
# Create cache with non-existent Redis server
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
# Should not raise exception when Redis unavailable
|
||||
cache.set(values) # Should silently fail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_unavailable_async(self):
|
||||
async def test_redis_unavailable_async(self) -> None:
|
||||
"""Test async behavior when Redis is unavailable."""
|
||||
# Create sync cache with non-existent Redis server (like main integration tests)
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
# Should return empty dict for get (delegates to sync)
|
||||
@@ -261,10 +265,10 @@ class TestRedisCache:
|
||||
# Should not raise exception for set (delegates to sync)
|
||||
await cache.aset(values) # Should silently fail
|
||||
|
||||
def test_corrupted_data_handling(self):
|
||||
def test_corrupted_data_handling(self) -> None:
|
||||
"""Test handling of corrupted data in Redis."""
|
||||
# Set some valid data first
|
||||
keys = [(("graph", "node"), "valid_key")]
|
||||
keys: list[FullKey] = [(("graph", "node"), "valid_key")]
|
||||
values = {keys[0]: ({"data": "valid"}, None)}
|
||||
self.cache.set(values)
|
||||
|
||||
@@ -273,33 +277,36 @@ class TestRedisCache:
|
||||
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
|
||||
|
||||
# Should skip corrupted entry and return only valid ones
|
||||
all_keys = [keys[0], (("graph", "node"), "corrupted_key")]
|
||||
all_keys: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")]
|
||||
result = self.cache.get(all_keys)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[keys[0]] == {"data": "valid"}
|
||||
|
||||
def test_key_parsing_edge_cases(self):
|
||||
def test_key_parsing_edge_cases(self) -> None:
|
||||
"""Test key parsing with edge cases."""
|
||||
# Test empty namespace
|
||||
key1 = ((), "empty_ns")
|
||||
key1: FullKey = ((), "empty_ns")
|
||||
values = {key1: ({"data": "empty_ns"}, None)}
|
||||
self.cache.set(values)
|
||||
result = self.cache.get([key1])
|
||||
assert result[key1] == {"data": "empty_ns"}
|
||||
|
||||
# Test namespace with special characters
|
||||
key2 = (("graph:with:colons", "node-with-dashes"), "key_with_underscores")
|
||||
key2: FullKey = (
|
||||
("graph:with:colons", "node-with-dashes"),
|
||||
"key_with_underscores",
|
||||
)
|
||||
values = {key2: ({"data": "special_chars"}, None)}
|
||||
self.cache.set(values)
|
||||
result = self.cache.get([key2])
|
||||
assert result[key2] == {"data": "special_chars"}
|
||||
|
||||
def test_large_data_serialization(self):
|
||||
def test_large_data_serialization(self) -> None:
|
||||
"""Test handling of large data objects."""
|
||||
# Create a large data structure
|
||||
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
|
||||
key = (("graph", "node"), "large_key")
|
||||
key: FullKey = (("graph", "node"), "large_key")
|
||||
values = {key: (large_data, None)}
|
||||
|
||||
self.cache.set(values)
|
||||
|
||||
@@ -845,7 +845,7 @@ async def test_async_batched_vector_search_concurrent(
|
||||
]
|
||||
)
|
||||
|
||||
for results, (query, filter_) in zip(all_results, search_queries):
|
||||
for results, (query, filter_) in zip(all_results, search_queries, strict=False):
|
||||
assert len(results) > 0, f"No results for query '{query}' with filter {filter_}"
|
||||
|
||||
for result in results:
|
||||
@@ -950,8 +950,8 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
|
||||
assert results[0].key != results[1].key
|
||||
ascore = results[0].score
|
||||
bscore = results[1].score
|
||||
assert ascore == bscore
|
||||
assert ascore is not None and bscore is not None
|
||||
assert ascore == pytest.approx(bscore, abs=1e-5)
|
||||
|
||||
results = await store.asearch(("test",), query="uuu")
|
||||
assert len(results) == 2
|
||||
@@ -1021,3 +1021,27 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
|
||||
assert len(results) == 3
|
||||
doc5_result = next(r for r in results if r.key == "doc5")
|
||||
assert doc5_result.score is None
|
||||
|
||||
|
||||
def test_non_ascii(fake_embeddings: CharacterEmbeddings) -> None:
|
||||
"""Test support for non-ascii characters"""
|
||||
store = InMemoryStore(
|
||||
index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
|
||||
)
|
||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
||||
store.put(("user_123", "memories"), "2", {"text": "これは日本語です"}) # Japanese
|
||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
||||
|
||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
||||
|
||||
assert result1[0].key == "1"
|
||||
assert result2[0].key == "2"
|
||||
assert result3[0].key == "3"
|
||||
assert result4[0].key == "4"
|
||||
assert result5[0].key == "5"
|
||||
|
||||
Generated
+496
-664
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, StateGraph, add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
model_oai = ChatOpenAI(temperature=0)
|
||||
|
||||
model_oai = model_oai.bind_tools(tools)
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state):
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
# If there are no tool calls, then we finish
|
||||
if not last_message.tool_calls:
|
||||
return "end"
|
||||
# Otherwise if there is, we continue
|
||||
else:
|
||||
return "continue"
|
||||
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state, config):
|
||||
model = model_oai
|
||||
messages = state["messages"]
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define the function to execute tools
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
|
||||
class ContextSchema(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.set_entry_point("agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
# Finally we pass in a mapping.
|
||||
# The keys are strings, and the values are other nodes.
|
||||
# END is a special node marking that the graph should finish.
|
||||
# What will happen is we will call `should_continue`, and then the output of that
|
||||
# will be matched against the keys in this mapping.
|
||||
# Based on which one it matches, that node will then be called.
|
||||
{
|
||||
# If `tools`, then we call the tool node.
|
||||
"continue": "action",
|
||||
# Otherwise we finish.
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("action", "agent")
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable
|
||||
graph = workflow.compile()
|
||||
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs-additional-deps"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph==0.6.0"
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs-zuper-deps"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==0.3.0"
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
".",
|
||||
"./deps/additional_deps",
|
||||
"./deps/zuper_deps"
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": "../.env"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==1.0.0a2",
|
||||
"langgraph==1.0.0a2",
|
||||
"langchain_community>=0.3.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "allow"
|
||||
@@ -0,0 +1,89 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, StateGraph, add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
model_oai = ChatOpenAI(temperature=0)
|
||||
|
||||
model_oai = model_oai.bind_tools(tools)
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state):
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
# If there are no tool calls, then we finish
|
||||
if not last_message.tool_calls:
|
||||
return "end"
|
||||
# Otherwise if there is, we continue
|
||||
else:
|
||||
return "continue"
|
||||
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state, config):
|
||||
model = model_oai
|
||||
messages = state["messages"]
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define the function to execute tools
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
|
||||
class ContextSchema(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.set_entry_point("agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
# Finally we pass in a mapping.
|
||||
# The keys are strings, and the values are other nodes.
|
||||
# END is a special node marking that the graph should finish.
|
||||
# What will happen is we will call `should_continue`, and then the output of that
|
||||
# will be matched against the keys in this mapping.
|
||||
# Based on which one it matches, that node will then be called.
|
||||
{
|
||||
# If `tools`, then we call the tool node.
|
||||
"continue": "action",
|
||||
# Otherwise we finish.
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("action", "agent")
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable
|
||||
graph = workflow.compile()
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
"."
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": "../.env"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==1.0.0a2",
|
||||
"langgraph==1.0.0a2",
|
||||
"langchain_community>=0.3.0",
|
||||
]
|
||||
@@ -7,6 +7,7 @@ from langchain_core.messages import BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, StateGraph, add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
@@ -17,6 +18,10 @@ model_anth = model_anth.bind_tools(tools)
|
||||
model_oai = model_oai.bind_tools(tools)
|
||||
|
||||
|
||||
class AgentContext(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
@@ -34,8 +39,8 @@ def should_continue(state):
|
||||
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state, config):
|
||||
if config["configurable"].get("model", "anthropic") == "anthropic":
|
||||
def call_model(state, runtime: Runtime[AgentContext]):
|
||||
if runtime.context.get("model", "anthropic") == "anthropic":
|
||||
model = model_anth
|
||||
else:
|
||||
model = model_oai
|
||||
@@ -49,12 +54,8 @@ def call_model(state, config):
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
|
||||
class ContextSchema(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
workflow = StateGraph(AgentState, context_schema=AgentContext)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
"langchain_community",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user