Compare commits

..
Author SHA1 Message Date
open-swe[bot] 7c09532a51 Apply patch [skip ci] 2025-08-22 15:24:37 +00:00
open-swe[bot] 98201fa98d Apply patch [skip ci] 2025-08-22 15:23:02 +00:00
open-swe[bot] 0334c81eb2 Apply patch [skip ci] 2025-08-22 15:22:51 +00:00
open-swe[bot] 0302621c24 Apply patch [skip ci] 2025-08-22 15:22:40 +00:00
open-swe[bot] 921a8edf66 Apply patch [skip ci] 2025-08-22 15:22:11 +00:00
open-swe[bot] dfee69f4b2 Apply patch [skip ci] 2025-08-22 15:22:00 +00:00
open-swe[bot] fc83746a29 Apply patch [skip ci] 2025-08-22 15:21:48 +00:00
open-swe[bot] 20befcb86a Apply patch [skip ci] 2025-08-22 15:20:42 +00:00
open-swe[bot] d97d18ad35 Apply patch [skip ci] 2025-08-22 15:20:31 +00:00
open-swe[bot] 2f096a3c2e Apply patch [skip ci] 2025-08-22 15:20:20 +00:00
open-swe[bot] d7084a4c83 Apply patch [skip ci] 2025-08-22 15:19:53 +00:00
open-swe[bot] 4b2fc401ff Apply patch [skip ci] 2025-08-22 15:19:41 +00:00
open-swe[bot] a3ebd02e71 Apply patch [skip ci] 2025-08-22 15:19:30 +00:00
open-swe[bot] c76431e57c Apply patch [skip ci] 2025-08-22 15:18:26 +00:00
open-swe[bot] 5b9e65bbbd Apply patch [skip ci] 2025-08-22 15:17:40 +00:00
open-swe[bot] 3326ac2e0d Apply patch [skip ci] 2025-08-22 15:17:30 +00:00
open-swe[bot] 630944b7dd Apply patch [skip ci] 2025-08-22 15:17:19 +00:00
open-swe[bot] 2fc293a2f4 Apply patch [skip ci] 2025-08-22 15:16:54 +00:00
open-swe[bot] f181695856 Apply patch [skip ci] 2025-08-22 15:16:28 +00:00
open-swe[bot] a7ab82fdd2 Apply patch [skip ci] 2025-08-22 15:16:18 +00:00
open-swe[bot] 5632cc003a Apply patch [skip ci] 2025-08-22 15:03:28 +00:00
open-swe[bot] 4d098c4cb5 Apply patch [skip ci] 2025-08-22 15:03:10 +00:00
open-swe[bot] 2d097ee9d1 Apply patch [skip ci] 2025-08-22 15:01:44 +00:00
open-swe[bot] 2f14631c84 Apply patch [skip ci] 2025-08-22 14:58:39 +00:00
open-swe[bot] 9b67fcfc87 Apply patch [skip ci] 2025-08-22 14:58:13 +00:00
open-swe[bot] cde4b9092f Apply patch [skip ci] 2025-08-22 14:57:10 +00:00
open-swe[bot] 620adb3bcf Apply patch [skip ci] 2025-08-22 14:56:50 +00:00
open-swe[bot] c8dd449d1a Apply patch [skip ci] 2025-08-22 14:56:02 +00:00
open-swe[bot] 6eef693d6e Apply patch [skip ci] 2025-08-22 14:55:46 +00:00
open-swe[bot] 193b4c34ec Apply patch [skip ci] 2025-08-22 14:54:53 +00:00
open-swe[bot] b242b5499c Apply patch [skip ci] 2025-08-22 14:54:29 +00:00
open-swe[bot] 93bdd275f0 Apply patch [skip ci] 2025-08-22 13:31:54 +00:00
open-swe[bot] 80c100273c Apply patch [skip ci] 2025-08-22 13:31:34 +00:00
open-swe[bot] 4096463586 Apply patch [skip ci] 2025-08-22 13:30:13 +00:00
open-swe[bot] dc446846e8 Apply patch [skip ci] 2025-08-22 13:29:36 +00:00
open-swe[bot] b3cbd71ed5 Apply patch [skip ci] 2025-08-22 13:28:58 +00:00
open-swe[bot] 3d2bc9069e Apply patch [skip ci] 2025-08-22 13:28:09 +00:00
open-swe[bot] 89e1933ab0 Apply patch [skip ci] 2025-08-22 13:27:53 +00:00
open-swe[bot] 5197b6261b Apply patch [skip ci] 2025-08-22 13:27:38 +00:00
open-swe[bot] bf2e321fb3 Apply patch [skip ci] 2025-08-22 13:27:22 +00:00
open-swe[bot] 767d34872f Apply patch [skip ci] 2025-08-22 13:27:08 +00:00
open-swe[bot] acdc1f7f6b Apply patch [skip ci] 2025-08-22 13:26:40 +00:00
open-swe[bot] 6c3f84bc4d Apply patch [skip ci] 2025-08-22 13:25:54 +00:00
open-swe[bot] 223150870c Apply patch [skip ci] 2025-08-22 13:23:45 +00:00
open-swe[bot] 50eb12d14f Apply patch [skip ci] 2025-08-22 13:22:13 +00:00
open-swe[bot] d8ce3d71cf Apply patch [skip ci] 2025-08-22 13:21:28 +00:00
open-swe[bot] 1adef5b6f1 Apply patch [skip ci] 2025-08-22 13:20:35 +00:00
open-swe[bot] 944947e889 Apply patch [skip ci] 2025-08-22 13:20:00 +00:00
open-swe[bot] a38727133b Apply patch [skip ci] 2025-08-22 13:19:24 +00:00
open-swe[bot] 5d929c30b4 Apply patch [skip ci] 2025-08-22 13:18:32 +00:00
open-swe[bot] fb091de4f2 Apply patch [skip ci] 2025-08-22 13:17:09 +00:00
open-swe[bot] 9eacc9ff67 Apply patch [skip ci] 2025-08-22 13:16:32 +00:00
open-swe[bot] 4e57940152 Apply patch [skip ci] 2025-08-22 13:15:41 +00:00
open-swe[bot] c227b0dea1 Apply patch [skip ci] 2025-08-22 13:14:05 +00:00
open-swe[bot] b6e8816e7d Apply patch [skip ci] 2025-08-22 13:13:15 +00:00
open-swe[bot] 779d454fcc Apply patch [skip ci] 2025-08-22 13:12:28 +00:00
open-swe[bot] 9423a99515 Apply patch [skip ci] 2025-08-22 13:11:55 +00:00
open-swe[bot] 4c04e023c8 Apply patch [skip ci] 2025-08-22 13:11:14 +00:00
open-swe[bot] 4566b2aed4 Apply patch [skip ci] 2025-08-22 13:10:45 +00:00
open-swe[bot] 55f6f052b6 Apply patch [skip ci] 2025-08-22 13:08:00 +00:00
open-swe[bot] c22a86f11e Apply patch [skip ci] 2025-08-22 13:06:23 +00:00
open-swe[bot] 6fee6811a0 Apply patch [skip ci] 2025-08-22 13:05:28 +00:00
open-swe[bot] 85180f5054 Apply patch [skip ci] 2025-08-22 13:02:01 +00:00
open-swe[bot] 6e30e457d4 Apply patch [skip ci] 2025-08-22 12:58:40 +00:00
open-swe[bot] f6512d5357 Apply patch [skip ci] 2025-08-22 12:58:19 +00:00
open-swe[bot] 3408e525f8 Apply patch [skip ci] 2025-08-22 12:57:52 +00:00
open-swe[bot] 777ab1123f Apply patch [skip ci] 2025-08-22 12:53:36 +00:00
open-swe[bot] a5a0286568 Apply patch [skip ci] 2025-08-22 12:52:47 +00:00
open-swe[bot] f86427fe37 Apply patch [skip ci] 2025-08-22 12:49:25 +00:00
open-swe[bot] 69f4ab0a79 Apply patch [skip ci] 2025-08-22 12:48:29 +00:00
open-swe[bot] 9284412aac Apply patch [skip ci] 2025-08-22 12:47:23 +00:00
open-swe[bot] 220b3b1d33 Apply patch [skip ci] 2025-08-22 12:47:00 +00:00
open-swe[bot] d0be6843ba Apply patch [skip ci] 2025-08-22 12:45:51 +00:00
open-swe[bot] 7fb9f7c870 Apply patch [skip ci] 2025-08-22 12:44:56 +00:00
open-swe[bot] cc54df296e Apply patch [skip ci] 2025-08-22 12:42:37 +00:00
open-swe[bot] a5b61361af Apply patch [skip ci] 2025-08-22 12:41:04 +00:00
open-swe[bot] 2f96c5c021 Apply patch [skip ci] 2025-08-22 12:40:44 +00:00
open-swe[bot] 0915784a87 Apply patch [skip ci] 2025-08-22 12:40:15 +00:00
open-swe[bot] 6952d3a1e0 Apply patch [skip ci] 2025-08-22 12:39:17 +00:00
open-swe[bot] d411013b6f Apply patch [skip ci] 2025-08-22 12:37:24 +00:00
open-swe[bot] ce9473293f Apply patch [skip ci] 2025-08-22 12:36:40 +00:00
open-swe[bot] 7fd8666608 Apply patch [skip ci] 2025-08-22 12:35:55 +00:00
open-swe[bot] adf42e7ca1 Apply patch [skip ci] 2025-08-22 12:35:09 +00:00
open-swe[bot] b0b6e3d91f Apply patch [skip ci] 2025-08-22 12:34:26 +00:00
open-swe[bot] 30e00b8c7b Apply patch [skip ci] 2025-08-22 12:33:34 +00:00
205 changed files with 5749 additions and 11799 deletions
+1 -4
View File
@@ -1,9 +1,6 @@
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 and support
about: General community discussions, support, and feature requests
+19
View File
@@ -0,0 +1,19 @@
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.
+42 -63
View File
@@ -14,25 +14,12 @@ 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@v5
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -46,65 +33,57 @@ jobs:
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 ${{ matrix.example.name }}
- name: Build and test service A
if: steps.changed-files.outputs.all
working-directory: ${{ matrix.example.workdir }}
working-directory: libs/cli/examples
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
# 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 }}
# 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
- 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 ]
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- "3.12"
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
with:
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
with:
+3 -3
View File
@@ -24,7 +24,7 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python $${ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v6
@@ -75,9 +75,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: astral-sh/setup-uv@v6
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- id: files
name: Get changed files
uses: Ana06/get-changed-files@v2.3.0
@@ -57,7 +57,7 @@ jobs:
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Annotation
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- 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@v5
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run check_sdk_methods script
@@ -118,7 +118,7 @@ jobs:
python-version:
- "3.11"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
with:
+1 -1
View File
@@ -21,7 +21,7 @@
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install Dependencies
run: |
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
changed-files: ${{ steps.changed-files.outputs.added_modified }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -41,7 +41,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -140,7 +140,7 @@ jobs:
- name: Upload Pages Artifact
# if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v3
with:
path: ./docs/site/
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1
+1 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Validate PR Title
uses: amannn/action-semantic-pull-request@v6
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -40,7 +40,6 @@ jobs:
sdk-py
docs
ci
deps
requireScope: false
ignoreLabels: |
ignore-lint-pr-title
+7 -7
View File
@@ -26,7 +26,7 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v6
@@ -87,7 +87,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
repository: langchain-ai/langgraph
path: langgraph
@@ -158,7 +158,7 @@ jobs:
- test-pypi-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -261,7 +261,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v6
@@ -270,7 +270,7 @@ jobs:
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -302,7 +302,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v6
@@ -311,7 +311,7 @@ jobs:
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- "latest"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Python + Poetry
uses: astral-sh/setup-uv@v6
with:
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v6
@@ -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`.
+3 -3
View File
@@ -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.
```python
my_function(1, "hello")
\```
.. code-block:: python
my_function(1, "hello")
Args:
arg1: This is a description of arg1. We do not need to specify the type since
+1 -1
View File
@@ -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/guides/): 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/how-tos/): 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.
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python
"""Debug why InjectedState deprecation warning is not being emitted."""
from typing import Annotated
from langgraph.prebuilt import ToolNode, InjectedState, InjectedStore
from langgraph.prebuilt.tool_node import _get_state_args, _get_store_arg, _get_reserved_keyword_args
from langgraph.store.base import BaseStore
from langchain_core.tools.base import create_schema_from_function
from langchain_core.tools import StructuredTool
def tool_with_injected_state(x: int, state: Annotated[dict, InjectedState]) -> str:
"""Tool using deprecated InjectedState annotation."""
return f"state: {state.get('foo', 'none')}"
def tool_with_injected_store(x: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
"""Tool using deprecated InjectedStore annotation."""
return "has store"
# Convert to tools
tool1 = create_tool(tool_with_injected_state)
tool2 = create_tool(tool_with_injected_store)
print("Debugging annotation detection:")
print(f"\nTool 1 (InjectedState):")
print(f" _get_state_args: {_get_state_args(tool1)}")
print(f" _get_reserved_keyword_args: {_get_reserved_keyword_args(tool1)}")
print(f"\nTool 2 (InjectedStore):")
print(f" _get_store_arg: {_get_store_arg(tool2)}")
print(f" _get_reserved_keyword_args: {_get_reserved_keyword_args(tool2)}")
# Check the condition for warnings
state_args = _get_state_args(tool1)
reserved_args = _get_reserved_keyword_args(tool1)
print(f"\nTool 1 warning condition:")
print(f" state_args: {state_args}")
print(f" reserved_args.get('state'): {reserved_args.get('state')}")
print(f" Should warn: {bool(state_args and not reserved_args.get('state'))}")
store_arg = _get_store_arg(tool2)
reserved_args2 = _get_reserved_keyword_args(tool2)
print(f"\nTool 2 warning condition:")
print(f" store_arg: {store_arg}")
print(f" reserved_args.get('runtime'): {reserved_args2.get('runtime')}")
print(f" Should warn: {bool(store_arg and not reserved_args2.get('runtime'))}")
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python
"""Debug why InjectedState warning is not being emitted."""
from typing import Annotated, get_args
from langgraph.prebuilt import InjectedState, InjectedStore
from langgraph.prebuilt.tool_node import get_all_basemodel_annotations, _is_injection
from langchain_core.tools import StructuredTool
from langgraph.store.base import BaseStore
def tool_with_injected_state(x: int, state: Annotated[dict, InjectedState]) -> str:
"""Tool using deprecated InjectedState annotation."""
return f"state: {state.get('foo', 'none')}"
def tool_with_injected_store(x: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
"""Tool using deprecated InjectedStore annotation."""
return "has store"
# Convert to tools
tool1 = StructuredTool.from_function(tool_with_injected_state)
tool2 = StructuredTool.from_function(tool_with_injected_store)
print("Debugging annotation detection:")
print(f"\nTool 1 (InjectedState):")
schema1 = tool1.get_input_schema()
print(f" Schema fields: {list(schema1.__fields__.keys()) if hasattr(schema1, '__fields__') else list(schema1.model_fields.keys())}")
for name, type_ in get_all_basemodel_annotations(schema1).items():
print(f" Field '{name}': type={type_}")
args = get_args(type_)
print(f" Type args: {args}")
for arg in args:
print(f" Is InjectedState? {_is_injection(arg, InjectedState)}")
print(f" Type of arg: {type(arg)}")
print(f"\nTool 2 (InjectedStore):")
schema2 = tool2.get_input_schema()
print(f" Schema fields: {list(schema2.__fields__.keys()) if hasattr(schema2, '__fields__') else list(schema2.model_fields.keys())}")
for name, type_ in get_all_basemodel_annotations(schema2).items():
print(f" Field '{name}': type={type_}")
args = get_args(type_)
print(f" Type args: {args}")
for arg in args:
print(f" Is InjectedStore? {_is_injection(arg, InjectedStore)}")
print(f" Type of arg: {type(arg)}")
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python
"""Debug reserved keyword detection for annotated tools."""
from typing import Annotated
from langgraph.prebuilt import InjectedState, InjectedStore
from langgraph.prebuilt.tool_node import _get_reserved_keyword_args
from langchain_core.tools import StructuredTool
from langgraph.store.base import BaseStore
def tool_with_injected_state(x: int, state: Annotated[dict, InjectedState]) -> str:
"""Tool using deprecated InjectedState annotation."""
return f"state: {state.get('foo', 'none')}"
def tool_with_injected_store(x: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
"""Tool using deprecated InjectedStore annotation."""
return "has store"
def tool_with_reserved_state(x: int, state) -> str:
"""Tool using reserved keyword 'state'."""
return f"state: {state.get('foo', 'none')}"
# Convert to tools
tool1 = StructuredTool.from_function(tool_with_injected_state)
tool2 = StructuredTool.from_function(tool_with_injected_store)
tool3 = StructuredTool.from_function(tool_with_reserved_state)
print("Debugging reserved keyword detection:")
print(f"\nTool 1 (Annotated[dict, InjectedState]):")
reserved1 = _get_reserved_keyword_args(tool1)
print(f" Reserved args: {reserved1}")
print(f" Has 'state' as reserved? {'state' in reserved1}")
print(f"\nTool 2 (Annotated[BaseStore, InjectedStore()]):")
reserved2 = _get_reserved_keyword_args(tool2)
print(f" Reserved args: {reserved2}")
print(f" Has 'runtime' as reserved? {'runtime' in reserved2}")
print(f"\nTool 3 (plain 'state' parameter):")
reserved3 = _get_reserved_keyword_args(tool3)
print(f" Reserved args: {reserved3}")
print(f" Has 'state' as reserved? {'state' in reserved3}")
# The issue might be that 'state' is being detected as a reserved keyword
# even when it has an annotation
print("\nConclusion:")
print("If tool1 shows 'state' as reserved, that's the bug - it shouldn't be")
print("considered reserved when it has an InjectedState annotation.")
+14 -117
View File
@@ -1,126 +1,24 @@
# LangGraph Documentation
# Setup
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md).
## 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
```
## Build Process
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:
To setup requirements for building docs you can run:
```bash
uv sync --group test
```
## Serving documentation locally
To run the documentation server locally you can run:
```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
```
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`
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
## Execute notebooks
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run:
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
```bash
python _scripts/prepare_notebooks_for_ci.py
@@ -135,9 +33,8 @@ 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
+2 -14
View File
@@ -1,5 +1,3 @@
"""Generate API reference links for imports in Python code blocks within markdown files."""
import ast
import importlib
import logging
@@ -72,18 +70,8 @@ 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"),
@@ -2108,9 +2108,9 @@ __metadata:
linkType: hard
"hono@npm:^4.5.4":
version: 4.9.7
resolution: "hono@npm:4.9.7"
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f
version: 4.8.9
resolution: "hono@npm:4.8.9"
checksum: 10c0/385539d1787fdc747bc869ef0e5ccc9f39cbe40289b94f23eecfc82c6ca440f059704647cd6381a5066d2cf7baa43ab25184c78d44af4c5c98a5c5b07670059e
languageName: node
linkType: hard
-2
View File
@@ -1,5 +1,3 @@
"""Convert Jupyter notebooks to markdown with custom processing."""
import ast
import os
import re
+7 -7
View File
@@ -144,13 +144,13 @@ REDIRECT_MAP = {
"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/use-studio#run-application",
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-assistants",
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-threads",
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/observability-studio#iterate-on-prompts",
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/observability-studio#run-experiments-over-a-dataset",
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/observability-studio#debug-langsmith-traces",
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langgraph-platform/observability-studio#add-node-to-dataset",
"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",
+2 -2
View File
@@ -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 "Added in version 0.6.0: `context` replaces `config['configurable']`"
!!! version-added "New in LangGraph v0.6: `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, runtime: Runtime[ContextSchema]):
def node(state: State, config: Runtime[ContextSchema]):
user_name = runtime.context.user_name
...
```
+1 -5
View File
@@ -211,7 +211,7 @@ output = agent.invoke(
print(output["messages"][-1].text())
```
!!! version-added "Added in version 0.6.0"
!!! version-added "New in LangGraph v0.6"
:::
@@ -351,13 +351,11 @@ 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`.
@@ -373,7 +371,6 @@ 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
@@ -384,5 +381,4 @@ 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/)
:::
+4 -2
View File
@@ -384,7 +384,7 @@ Putting this together, here is how you can implement a simple multi-agent system
```python
from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command
@@ -395,7 +395,7 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
@tool(name, description=description)
def handoff_tool(
# highlight-next-line
state: Annotated[MessagesState, InjectedState], # (1)!
state, # (1)! Reserved keyword - automatically injected
# highlight-next-line
tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
@@ -636,3 +636,5 @@ Check out LangGraph [supervisor](https://github.com/langchain-ai/langgraph-super
:::js
Check out LangGraph [supervisor](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-supervisor#customizing-handoff-tools) and [swarm](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-swarm#customizing-handoff-tools) documentation to learn how to customize handoffs.
:::
+2 -2
View File
@@ -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 queue subsequent updates. Once a build completes, the most recent commit will begin building and the other queued builds will be skipped.
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.
## Add or Remove GitHub Repositories
+1 -331
View File
@@ -29,10 +29,6 @@
"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."
@@ -1524,96 +1520,6 @@
}
}
},
"/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": [
@@ -3186,195 +3092,6 @@
}
}
},
"/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",
@@ -4629,17 +4346,6 @@
"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",
@@ -4876,17 +4582,6 @@
"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",
@@ -5015,12 +4710,6 @@
},
"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",
@@ -5261,30 +4950,11 @@
"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 updating a thread."
"description": "Payload for creating a thread."
},
"ThreadStateCheckpointRequest": {
"properties": {
+3 -3
View File
@@ -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"
+1 -1
View File
@@ -1040,7 +1040,7 @@ def node_a(state: State, runtime: Runtime[ContextSchema]):
...
```
See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full breakdown on configuration.
See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
:::
:::js
+2 -2
View File
@@ -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=current_instructions.value["instructions"], conversation=state["messages"])
prompt = prompt_template.format(instructions=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.
+7 -7
View File
@@ -498,15 +498,14 @@ In this variant of the [supervisor](#supervisor) architecture, we define a super
:::python
```python
from typing import Annotated
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import InjectedState, create_react_agent
from langgraph.prebuilt import create_react_agent
model = ChatOpenAI()
# this is the agent function that will be called as tool
# notice that you can pass the state to the tool via InjectedState annotation
def agent_1(state: Annotated[dict, InjectedState]):
# notice that you can pass the state to the tool via the reserved keyword 'state'
def agent_1(state): # 'state' is a reserved keyword - automatically injected
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
# and add any additional logic (different models, custom prompts, structured output, etc.)
response = model.invoke(...)
@@ -515,7 +514,7 @@ def agent_1(state: Annotated[dict, InjectedState]):
# by the prebuilt create_react_agent (supervisor)
return response.content
def agent_2(state: Annotated[dict, InjectedState]):
def agent_2(state): # 'state' is a reserved keyword - automatically injected
response = model.invoke(...)
return response.content
@@ -897,5 +896,6 @@ 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.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.
- 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.
+14 -13
View File
@@ -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]}, context=ContextSchema())["messages"][-1]
response_1 = graph.invoke({"messages": [input_message]})["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.types import RetryPolicy
from langgraph.pregel 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.types import RetryPolicy
from langgraph.pregel 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,6 +2110,7 @@ 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()
```
@@ -2332,7 +2333,7 @@ from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
```
![Simple loop graph](assets/graph_api_image_7.png)
![Simple loop graph](assets/graph_api_image_3.png)
:::
:::js
@@ -3271,7 +3272,7 @@ from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeSt
display(Image(app.get_graph().draw_mermaid_png()))
```
![Fractal graph visualization](assets/graph_api_image_10.png)
![Fractal graph visualization](assets/graph_api_image_5.png)
**Using Mermaid + Pyppeteer**
@@ -3319,4 +3320,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.id: f"edited text for {i.value['text_to_revise']}"
for i in graph.get_state(config).interrupts
i.interrupt_id: f"human input for prompt {i.value}"
for i in parent.get_state(thread_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'}
+13 -9
View File
@@ -26,7 +26,7 @@ To implement handoffs, you can return `Command` objects from your agent nodes or
```python
from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command
@@ -37,7 +37,7 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
@tool(name, description=description)
def handoff_tool(
# highlight-next-line
state: Annotated[MessagesState, InjectedState], # (1)!
state, # (1)! Reserved keyword - automatically injected
# highlight-next-line
tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
@@ -58,7 +58,7 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
return handoff_tool
```
1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool using the @[InjectedState] annotation.
1. Access the [state](../concepts/low_level.md#state) of the agent using the reserved keyword `state`. No annotation needed - LangGraph automatically injects the state when it sees this parameter name.
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
3. Name of the agent or node to hand off to.
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
@@ -183,7 +183,6 @@ You can use the @[`Send()`][Send] primitive to directly send data to the worker
from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langgraph.graph import StateGraph, START, MessagesState
# highlight-next-line
from langgraph.types import Command, Send
@@ -202,7 +201,7 @@ def create_task_description_handoff_tool(
"Description of what the next agent should do, including all of the relevant context.",
],
# these parameters are ignored by the LLM
state: Annotated[MessagesState, InjectedState],
state, # Reserved keyword - automatically injected
) -> Command:
task_description_message = {"role": "user", "content": task_description}
agent_input = {**state, "messages": [task_description_message]}
@@ -382,7 +381,7 @@ const multiAgentGraph = new StateGraph(MessagesZodState)
from typing import Annotated
from langchain_core.messages import convert_to_messages
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command
@@ -435,7 +434,7 @@ const multiAgentGraph = new StateGraph(MessagesZodState)
@tool(name, description=description)
def handoff_tool(
# highlight-next-line
state: Annotated[MessagesState, InjectedState], # (1)!
state, # (1)! Reserved keyword - automatically injected
# highlight-next-line
tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
@@ -789,7 +788,7 @@ function agent(state: MessagesState): Command {
```python
from langchain_anthropic import ChatAnthropic
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import InMemorySaver
@@ -1250,4 +1249,9 @@ LangGraph comes with prebuilt implementations of two of the most popular multi-a
:::js
- [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-js) library to create a supervisor multi-agent systems.
- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-js) library to create a swarm multi-agent systems.
:::
:::
+9 -5
View File
@@ -244,7 +244,7 @@ output = agent.invoke(
print(output["messages"][-1].text())
```
!!! version-added "Added in version 0.6.0"
!!! version-added "New in langgraph>=0.6"
:::
@@ -1141,12 +1141,12 @@ await agent.invoke(
Short-term memory maintains **dynamic** state that changes during a single execution.
:::python
To **access** (read) the graph state inside the tools, you can use a special parameter **annotation** — @[`InjectedState`][InjectedState]:
To **access** (read) the graph state inside the tools, you can use the reserved keyword parameter `state`:
```python
from typing import Annotated, NotRequired
from typing import NotRequired
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState, create_react_agent
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
class CustomState(AgentState):
@@ -1156,7 +1156,7 @@ class CustomState(AgentState):
@tool
def get_user_name(
# highlight-next-line
state: Annotated[CustomState, InjectedState]
state # Reserved keyword - automatically injected
) -> str:
"""Retrieve the current user-name from state."""
# Return stored name or a default if not set
@@ -1173,6 +1173,9 @@ agent = create_react_agent(
agent.invoke({"messages": "what's my name?"})
```
!!! note "Migration from Annotations"
The old annotation-based approach using `Annotated[CustomState, InjectedState]` is deprecated but still supported for backward compatibility. Simply use the reserved keyword `state` without any type annotation for cleaner code.
:::
:::js
@@ -2387,3 +2390,4 @@ Some commonly used tool categories include:
These integrations can be configured and added to your agents using the same `tools` parameter shown in the examples above.
:::
@@ -383,7 +383,6 @@ We will implement handoffs via **handoff tools** and give these tools to the sup
```python
from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command
@@ -394,7 +393,7 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
@tool(name, description=description)
def handoff_tool(
state: Annotated[MessagesState, InjectedState],
state, # Reserved keyword - automatically injected
tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
tool_message = {
@@ -644,7 +643,7 @@ def create_task_description_handoff_tool(
"Description of what the next agent should do, including all of the relevant context.",
],
# these parameters are ignored by the LLM
state: Annotated[MessagesState, InjectedState],
state, # Reserved keyword - automatically injected
) -> Command:
task_description_message = {"role": "user", "content": task_description}
agent_input = {**state, "messages": [task_description_message]}
@@ -766,3 +765,5 @@ Update from subgraph research_agent:
{"query": "2024 United States GDP value from a reputable source", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.focus-economics.com/countries/united-states/", "title": "United States Economy Overview - Focus Economics", "content": "The United States' Macroeconomic Analysis:\n------------------------------------------\n\n**Nominal GDP of USD 29,185 billion in 2024.**\n\n**Nominal GDP of USD 29,179 billion in 2024.**\n\n**GDP per capita of USD 86,635 compared to the global average of USD 10,589.**\n\n**GDP per capita of USD 86,652 compared to the global average of USD 10,589.**\n\n**Average real GDP growth of 2.5% over the last decade.**\n\n**Average real GDP growth of ```
```
+1 -1
View File
@@ -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["Action", END]:
def should_continue(state: MessagesState) -> Literal["environment", END]:
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
messages = state["messages"]
+2 -2
View File
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
}
.md-banner {
background-color: #FFAE42;
background-color: #CFC9FA;
color: #000000;
}
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
{% endblock %}
{% block announce %}
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>
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>.
{% endblock %}
+4 -4
View File
@@ -7,14 +7,14 @@ name = "langgraph-docs"
version = "0.0.1"
description = "LangGraph docs"
authors = []
requires-python = ">=3.11.0,<4.0.0"
requires-python = "~=3.11"
readme = "README.md"
license = "MIT"
dependencies = [
"aiohappyeyeballs==2.4.3",
"hub>=3.0.1,<4.0.0",
"xxhash>=3.5.0,<4.0.0",
"black>=25.1.0,<26.0.0",
"hub>=3.0.1,<4",
"xxhash>=3.5.0,<4",
"black>=25.1.0,<26",
]
[dependency-groups]
Generated
+4 -5
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
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.7"
version = "0.6.2"
source = { editable = "../libs/langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -2380,7 +2380,6 @@ dev = [
{ name = "pytest-repeat" },
{ name = "pytest-watcher" },
{ name = "pytest-xdist", extras = ["psutil"] },
{ name = "redis" },
{ name = "ruff" },
{ name = "syrupy" },
{ name = "types-requests" },
@@ -2414,7 +2413,6 @@ dev = [
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
{ name = "pytest-watcher" },
{ name = "redis" },
{ name = "ruff" },
]
@@ -2645,7 +2643,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "0.6.4"
version = "0.6.2"
source = { editable = "../libs/prebuilt" }
dependencies = [
{ name = "langchain-core" },
@@ -2676,6 +2674,7 @@ dev = [
[[package]]
name = "langgraph-sdk"
version = "0.2.0"
source = { editable = "../libs/sdk-py" }
dependencies = [
{ name = "httpx" },
+1 -1
View File
@@ -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/memory/add-memory.md"
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
]
}
],
+1 -3
View File
@@ -707,9 +707,7 @@
" \"\"\"\n",
" Find all tool calls in the messages returned\n",
" \"\"\"\n",
" tool_calls = [\n",
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
" ]\n",
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
" return tool_calls\n",
"\n",
"\n",
@@ -7,6 +7,11 @@ 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,
@@ -14,17 +19,12 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_serializable_checkpoint_metadata,
get_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
@@ -325,7 +325,7 @@ class PostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(copy),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
Jsonb(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -450,7 +450,7 @@ class PostgresSaver(BasePostgresSaver):
{
**value["checkpoint"],
"channel_values": {
**(value["checkpoint"].get("channel_values") or {}),
**value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]),
},
},
@@ -7,6 +7,11 @@ 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,
@@ -14,17 +19,12 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_serializable_checkpoint_metadata,
get_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
@@ -283,7 +283,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(copy),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
Jsonb(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -409,7 +409,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
{
**value["checkpoint"],
"channel_values": {
**(value["checkpoint"].get("channel_values") or {}),
**value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]),
},
},
@@ -1,12 +1,12 @@
from __future__ import annotations
import random
import warnings
from collections.abc import Sequence
from importlib.metadata import version as get_version
from typing import Any, Optional, cast
from langchain_core.runnables import RunnableConfig
from psycopg.types.json import Jsonb
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
@@ -14,22 +14,9 @@ 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]]
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.
The position of the migration in the list is the version number.
@@ -6,16 +6,6 @@ from contextlib import asynccontextmanager, contextmanager
from typing import Any, Optional
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,
@@ -29,8 +19,18 @@ 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.
@@ -441,7 +441,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(copy),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
Jsonb(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -774,7 +774,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(copy),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
Jsonb(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -1,4 +1,4 @@
from langgraph.store.postgres.aio import AsyncPostgresStore
from langgraph.store.postgres.base import PoolConfig, PostgresStore
from langgraph.store.postgres.base import PostgresStore
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
__all__ = ["AsyncPostgresStore", "PostgresStore"]
@@ -8,6 +8,11 @@ from types import TracebackType
from typing import Any, Callable, 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,
@@ -17,11 +22,6 @@ 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,
@@ -22,6 +22,14 @@ from typing import (
)
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,
@@ -38,14 +46,6 @@ 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
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-postgres"
version = "2.0.25"
version = "2.0.23"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.9"
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.1.2,<3.0.0",
"langgraph-checkpoint>=2.0.21,<3.0.0",
"orjson>=3.10.1",
"psycopg>=3.2.0",
"psycopg-pool>=3.2.0",
+9 -36
View File
@@ -6,6 +6,10 @@ 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,
@@ -13,15 +17,11 @@ 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,11 +187,13 @@ 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 = {}
@@ -218,6 +220,7 @@ 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, {})
@@ -243,6 +246,7 @@ 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
@@ -340,34 +344,3 @@ 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"] == {}
@@ -12,6 +12,8 @@ from typing import Any
import pytest
from langchain_core.embeddings import Embeddings
from psycopg import AsyncConnection
from langgraph.store.base import (
GetOp,
Item,
@@ -19,8 +21,6 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from psycopg import AsyncConnection
from langgraph.store.postgres import AsyncPostgresStore
from tests.conftest import (
DEFAULT_URI,
+2 -40
View File
@@ -9,6 +9,8 @@ from uuid import uuid4
import pytest
from langchain_core.embeddings import Embeddings
from psycopg import Connection
from langgraph.store.base import (
GetOp,
Item,
@@ -17,8 +19,6 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from psycopg import Connection
from langgraph.store.postgres import PostgresStore
from tests.conftest import (
DEFAULT_URI,
@@ -861,41 +861,3 @@ 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"
+9 -35
View File
@@ -7,6 +7,10 @@ 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,
@@ -14,12 +18,8 @@ from langgraph.checkpoint.base import (
create_checkpoint,
empty_checkpoint,
)
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 langgraph.checkpoint.serde.types import TASKS
from tests.conftest import DEFAULT_POSTGRES_URI
@@ -169,11 +169,13 @@ 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 = {}
@@ -200,6 +202,7 @@ 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, {})
@@ -225,6 +228,7 @@ 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
@@ -328,33 +332,3 @@ 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"] == {}
+500 -485
View File
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@ 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,
@@ -20,7 +21,6 @@ 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 = (
@@ -8,6 +8,7 @@ from typing import Any, Callable, TypeVar, cast
import aiosqlite
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
@@ -20,7 +21,6 @@ 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)
@@ -5,6 +5,7 @@ from collections.abc import Sequence
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import get_checkpoint_id
@@ -11,6 +11,7 @@ from typing import Any, Callable, cast
import aiosqlite
import orjson
import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import (
GetOp,
ListNamespacesOp,
@@ -21,7 +22,6 @@ from langgraph.store.base import (
TTLConfig,
)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.sqlite.base import (
_PLACEHOLDER,
BaseSqliteStore,
@@ -507,9 +507,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
results: List to store results in.
cur: Database cursor.
"""
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
search_ops
)
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:
@@ -517,60 +515,23 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
[query for _, query in embedding_requests]
)
for (embed_req_idx, _), embedding in zip(embedding_requests, vectors):
# 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, _), 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 (original_op_idx, _), (query, params, needs_refresh) in zip(
search_ops, prepared_queries
):
for (idx, _), (query, params) in zip(search_ops, queries):
await cur.execute(query, params)
rows = await cur.fetchall()
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
if "score" in query:
items = [
_row_to_search_item(
_decode_ns_text(row[0]), # prefix
_decode_ns_text(row[0]),
{
"key": row[1], # key
"value": row[2], # value
"key": row[1],
"value": row[2],
"created_at": row[3],
"updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None,
@@ -584,10 +545,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
else: # Regular search query
items = [
_row_to_search_item(
_decode_ns_text(row[0]), # prefix
_decode_ns_text(row[0]),
{
"key": row[1], # key
"value": row[2], # value
"key": row[1],
"value": row[2],
"created_at": row[3],
"updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None,
@@ -598,7 +559,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
for row in rows
]
results[original_op_idx] = items
results[idx] = items
async def _batch_list_namespaces_ops(
self,
@@ -13,6 +13,7 @@ from typing import Any, Callable, Literal, NamedTuple, cast
import orjson
import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import (
BaseStore,
GetOp,
@@ -371,15 +372,13 @@ class BaseSqliteStore:
def _prepare_batch_search_queries(
self, search_ops: Sequence[tuple[int, SearchOp]]
) -> tuple[
list[
tuple[str, list[None | str | list[float]], bool]
], # queries, params, needs_refresh
list[tuple[str, list[None | str | list[float]]]], # queries, params
list[tuple[int, str]], # idx, query_text pairs to embed
]:
"""
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
Returns:
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
- queries: list of (SQL, param_list)
- embedding_requests: list of (original_index_in_search_ops, text_query)
"""
queries = []
@@ -520,18 +519,30 @@ class BaseSqliteStore:
logger.debug(f"Search query: {base_query}")
logger.debug(f"Search params: {params}")
# Determine if TTL refresh is needed
needs_ttl_refresh = bool(
# Handle TTL refresh if requested
if (
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
# 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))
queries.append((final_sql, final_params))
return queries, embedding_requests
@@ -1320,9 +1331,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
results: list[Result],
cur: sqlite3.Cursor,
) -> None:
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
search_ops
)
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
# Setup similarity functions if they don't exist
if embedding_requests and self.embeddings:
@@ -1332,48 +1341,16 @@ class SqliteStore(BaseSqliteStore, BaseStore):
)
# Replace placeholders with actual embeddings
for (embed_req_idx, _), embedding in zip(embedding_requests, embeddings):
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, _), 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 (original_op_idx, _), (query, params, needs_refresh) in zip(
search_ops, prepared_queries
):
for (idx, _), (query, params) in zip(search_ops, queries):
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(
@@ -1408,7 +1385,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
for row in rows
]
results[original_op_idx] = items
results[idx] = items
def _batch_list_namespaces_ops(
self,
@@ -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
@@ -8,6 +8,7 @@ from contextlib import asynccontextmanager
from typing import Optional, Union, cast
import pytest
from langgraph.store.base import (
GetOp,
Item,
@@ -15,7 +16,6 @@ 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
+2 -12
View File
@@ -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,17 +116,7 @@ class TestSqliteSaver:
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# 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"
# TODO: test before and limit params
def test_search_where(self) -> None:
# call method / assertions
+1 -29
View File
@@ -9,6 +9,7 @@ from typing import Any, Literal, Optional, Union, cast
import pytest
from langchain_core.embeddings import Embeddings
from langgraph.store.base import (
GetOp,
Item,
@@ -17,7 +18,6 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig
@@ -1067,31 +1067,3 @@ 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"
+2 -76
View File
@@ -7,7 +7,6 @@ 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
@@ -94,13 +93,9 @@ 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=ttl_config,
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
) as store:
store.setup()
@@ -303,14 +298,9 @@ 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=ttl_config,
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
) as store:
await store.setup()
@@ -363,67 +353,3 @@ 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"
)
+440 -426
View File
File diff suppressed because it is too large Load Diff
@@ -404,16 +404,6 @@ 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.
@@ -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, ensure_ascii=False)]
return [json.dumps(obj, sort_keys=True)]
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, ensure_ascii=False)]
return [json.dumps(obj, sort_keys=True)]
return []
token = tokens[pos]
@@ -295,11 +295,7 @@ 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, ensure_ascii=False
)
)
results.append(json.dumps(current_obj, sort_keys=True))
# Handle wildcard
elif token == "*":
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "2.1.2"
version = "2.1.1"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.9"
+42 -49
View File
@@ -5,13 +5,12 @@ 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) -> None:
def setup(self):
"""Set up test Redis client and cache."""
self.client = redis.Redis(
host="localhost", port=6379, db=0, decode_responses=False
@@ -21,21 +20,21 @@ class TestRedisCache:
except redis.ConnectionError:
pytest.skip("Redis server not available")
self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:")
self.cache = RedisCache(self.client, prefix="test:cache:")
# Clean up before each test
self.client.flushdb()
def teardown_method(self) -> None:
def teardown_method(self):
"""Clean up after each test."""
try:
self.client.flushdb()
except Exception:
pass
def test_basic_set_and_get(self) -> None:
def test_basic_set_and_get(self):
"""Test basic set and get operations."""
keys: list[FullKey] = [(("graph", "node"), "key1")]
keys = [(("graph", "node"), "key1")]
values = {keys[0]: ({"result": 42}, None)}
# Set value
@@ -46,9 +45,9 @@ class TestRedisCache:
assert len(result) == 1
assert result[keys[0]] == {"result": 42}
def test_batch_operations(self) -> None:
def test_batch_operations(self):
"""Test batch set and get operations."""
keys: list[FullKey] = [
keys = [
(("graph", "node1"), "key1"),
(("graph", "node2"), "key2"),
(("other", "node"), "key3"),
@@ -69,9 +68,9 @@ class TestRedisCache:
assert result[keys[1]] == {"result": 2}
assert result[keys[2]] == {"result": 3}
def test_ttl_behavior(self) -> None:
def test_ttl_behavior(self):
"""Test TTL (time-to-live) functionality."""
key: FullKey = (("graph", "node"), "ttl_key")
key = (("graph", "node"), "ttl_key")
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
# Set with TTL
@@ -89,10 +88,10 @@ class TestRedisCache:
result = self.cache.get([key])
assert len(result) == 0
def test_namespace_isolation(self) -> None:
def test_namespace_isolation(self):
"""Test that different namespaces are isolated."""
key1: FullKey = (("graph1", "node"), "same_key")
key2: FullKey = (("graph2", "node"), "same_key")
key1 = (("graph1", "node"), "same_key")
key2 = (("graph2", "node"), "same_key")
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
@@ -102,12 +101,9 @@ class TestRedisCache:
assert result[key1] == {"graph": 1}
assert result[key2] == {"graph": 2}
def test_clear_all(self) -> None:
def test_clear_all(self):
"""Test clearing all cached values."""
keys: list[FullKey] = [
(("graph", "node1"), "key1"),
(("graph", "node2"), "key2"),
]
keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
self.cache.set(values)
@@ -123,9 +119,9 @@ class TestRedisCache:
result = self.cache.get(keys)
assert len(result) == 0
def test_clear_by_namespace(self) -> None:
def test_clear_by_namespace(self):
"""Test clearing cached values by namespace."""
keys: list[FullKey] = [
keys = [
(("graph1", "node"), "key1"),
(("graph2", "node"), "key2"),
(("graph1", "other"), "key3"),
@@ -146,7 +142,7 @@ class TestRedisCache:
assert len(result) == 1
assert result[keys[1]] == {"result": 2}
def test_empty_operations(self) -> None:
def test_empty_operations(self):
"""Test behavior with empty keys/values."""
# Empty get
result = self.cache.get([])
@@ -155,14 +151,14 @@ class TestRedisCache:
# Empty set
self.cache.set({}) # Should not raise error
def test_nonexistent_keys(self) -> None:
def test_nonexistent_keys(self):
"""Test getting keys that don't exist."""
keys: list[FullKey] = [(("graph", "node"), "nonexistent")]
keys = [(("graph", "node"), "nonexistent")]
result = self.cache.get(keys)
assert len(result) == 0
@pytest.mark.asyncio
async def test_async_operations(self) -> None:
async def test_async_operations(self):
"""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)
@@ -171,9 +167,9 @@ class TestRedisCache:
except Exception:
pytest.skip("Redis not available")
cache: RedisCache = RedisCache(client, prefix="test:async:")
cache = RedisCache(client, prefix="test:async:")
keys: list[FullKey] = [(("graph", "node"), "async_key")]
keys = [(("graph", "node"), "async_key")]
values = {keys[0]: ({"async": True}, None)}
# Async set (delegates to sync)
@@ -188,7 +184,7 @@ class TestRedisCache:
client.flushdb()
@pytest.mark.asyncio
async def test_async_clear(self) -> None:
async def test_async_clear(self):
"""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)
@@ -197,9 +193,9 @@ class TestRedisCache:
except Exception:
pytest.skip("Redis not available")
cache: RedisCache = RedisCache(client, prefix="test:async:")
cache = RedisCache(client, prefix="test:async:")
keys: list[FullKey] = [(("graph", "node"), "key")]
keys = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)}
await cache.aset(values)
@@ -218,44 +214,44 @@ class TestRedisCache:
# Cleanup
client.flushdb()
def test_redis_unavailable_get(self) -> None:
def test_redis_unavailable_get(self):
"""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 = RedisCache(bad_client, prefix="test:cache:")
cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")]
keys = [(("graph", "node"), "key")]
result = cache.get(keys)
# Should return empty dict when Redis unavailable
assert result == {}
def test_redis_unavailable_set(self) -> None:
def test_redis_unavailable_set(self):
"""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 = RedisCache(bad_client, prefix="test:cache:")
cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")]
keys = [(("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) -> None:
async def test_redis_unavailable_async(self):
"""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 = RedisCache(bad_client, prefix="test:cache:")
cache = RedisCache(bad_client, prefix="test:cache:")
keys: list[FullKey] = [(("graph", "node"), "key")]
keys = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)}
# Should return empty dict for get (delegates to sync)
@@ -265,10 +261,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) -> None:
def test_corrupted_data_handling(self):
"""Test handling of corrupted data in Redis."""
# Set some valid data first
keys: list[FullKey] = [(("graph", "node"), "valid_key")]
keys = [(("graph", "node"), "valid_key")]
values = {keys[0]: ({"data": "valid"}, None)}
self.cache.set(values)
@@ -277,36 +273,33 @@ 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: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")]
all_keys = [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) -> None:
def test_key_parsing_edge_cases(self):
"""Test key parsing with edge cases."""
# Test empty namespace
key1: FullKey = ((), "empty_ns")
key1 = ((), "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: FullKey = (
("graph:with:colons", "node-with-dashes"),
"key_with_underscores",
)
key2 = (("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) -> None:
def test_large_data_serialization(self):
"""Test handling of large data objects."""
# Create a large data structure
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
key: FullKey = (("graph", "node"), "large_key")
key = (("graph", "node"), "large_key")
values = {key: (large_data, None)}
self.cache.set(values)
+1 -25
View File
@@ -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,27 +1021,3 @@ 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"
+544 -553
View File
File diff suppressed because it is too large Load Diff
@@ -1,89 +0,0 @@
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()
@@ -1,9 +0,0 @@
[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"
]
@@ -1,9 +0,0 @@
[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"
]
@@ -1,13 +0,0 @@
{
"python_version": "3.12",
"dependencies": [
".",
"./deps/additional_deps",
"./deps/zuper_deps"
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -1,14 +0,0 @@
[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"
@@ -1,89 +0,0 @@
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()
@@ -1,11 +0,0 @@
{
"python_version": "3.12",
"dependencies": [
"."
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -1,11 +0,0 @@
[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 -8
View File
@@ -7,7 +7,6 @@ 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)]
@@ -18,10 +17,6 @@ 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]
@@ -39,8 +34,8 @@ def should_continue(state):
# Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]):
if runtime.context.get("model", "anthropic") == "anthropic":
def call_model(state, config):
if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth
else:
model = model_oai
@@ -54,8 +49,12 @@ def call_model(state, runtime: Runtime[AgentContext]):
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext)
workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
-1
View File
@@ -1,5 +1,4 @@
{
"$schema": "https://langgra.ph/schema.json",
"python_version": "3.12",
"dependencies": [
"langchain_community",
@@ -1,6 +1,6 @@
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated, Literal, TypedDict
from typing import Annotated, TypedDict
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
@@ -8,7 +8,6 @@ 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)]
@@ -22,10 +21,6 @@ prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
subprompt = open(Path(__file__).parent / "subprompt.txt").read()
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -43,8 +38,8 @@ def should_continue(state):
# Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]):
if runtime.context.get("model", "anthropic") == "anthropic":
def call_model(state, config):
if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth
else:
model = model_oai
@@ -57,8 +52,9 @@ def call_model(state, runtime: Runtime[AgentContext]):
# Define the function to execute tools
tool_node = ToolNode(tools)
# Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext)
workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
@@ -1,5 +1,4 @@
{
"$schema": "https://langgra.ph/schema.json",
"dependencies": [
"."
],
@@ -1,6 +1,6 @@
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated, Literal, TypedDict
from typing import Annotated, TypedDict
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
@@ -8,7 +8,6 @@ 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)]
@@ -22,10 +21,6 @@ prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
subprompt = open(Path(__file__).parent / "subprompt.txt").read()
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -43,8 +38,8 @@ def should_continue(state):
# Define the function that calls the model
def call_model(state, runtime: Runtime[AgentContext]):
if runtime.context.get("model", "anthropic") == "anthropic":
def call_model(state, config):
if config["configurable"].get("model", "anthropic") == "anthropic":
model = model_anth
else:
model = model_oai
@@ -59,7 +54,7 @@ tool_node = ToolNode(tools)
# Define a new graph
workflow = StateGraph(AgentState, context_schema=AgentContext)
workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
@@ -1,5 +1,4 @@
{
"$schema": "https://langgra.ph/schema.json",
"dependencies": [
"."
],
-1
View File
@@ -1,5 +1,4 @@
{
"$schema": "https://langgra.ph/schema.json",
"node_version": "20",
"graphs": {
"agent": "./src/agent/graph.ts:graph"
@@ -1,62 +0,0 @@
module.exports = {
extends: [
"eslint:recommended",
"prettier",
"plugin:@typescript-eslint/recommended",
],
parserOptions: {
ecmaVersion: 12,
parser: "@typescript-eslint/parser",
project: "./tsconfig.json",
sourceType: "module",
},
plugins: ["import", "@typescript-eslint", "no-instanceof"],
ignorePatterns: [
".eslintrc.cjs",
"scripts",
"src/utils/lodash/*",
"node_modules",
"dist",
"dist-cjs",
"*.js",
"*.cjs",
"*.d.ts",
],
rules: {
"no-process-env": 2,
"no-instanceof/no-instanceof": 2,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-shadow": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
camelcase: 0,
"class-methods-use-this": 0,
"import/extensions": [2, "ignorePackages"],
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts"] },
],
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"keyword-spacing": "error",
"max-classes-per-file": 0,
"max-len": 0,
"no-await-in-loop": 0,
"no-bitwise": 0,
"no-console": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-continue": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-useless-constructor": 0,
"no-return-await": 0,
"consistent-return": 0,
"no-else-return": 0,
"new-cap": ["error", { properties: false, capIsNew: false }],
},
};
@@ -1,7 +0,0 @@
{
"node_version": "20",
"graphs": {
"agent": "./src/graph.ts:graph"
},
"env": "../../.env"
}
@@ -1,18 +0,0 @@
{
"name": "@js-monorepo-example/agent",
"version": "0.0.1",
"type": "module",
"main": "src/graph.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"dependencies": {
"@js-monorepo-example/shared": "*",
"@langchain/core": "^0.3.2",
"@langchain/langgraph": "^0.2.5"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
@@ -1,47 +0,0 @@
/**
* Simple LangGraph.js example for monorepo testing
*/
import { StateGraph } from "@langchain/langgraph";
import { RunnableConfig } from "@langchain/core/runnables";
import { StateAnnotation } from "./state.js";
import { getGreeting } from "@js-monorepo-example/shared";
/**
* Simple node that uses the shared library
*/
const callModel = async (
state: typeof StateAnnotation.State,
_config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
// Use functions from the shared library
const greeting = getGreeting();
return {
messages: [
{
role: "assistant",
content: `${greeting}`,
},
],
};
};
/**
* Simple routing function
*/
export const route = (
state: typeof StateAnnotation.State,
): "__end__" | "callModel" => {
if (state.messages.length > 0) {
return "__end__";
}
return "callModel";
};
// Create the graph
const builder = new StateGraph(StateAnnotation)
.addNode("callModel", callModel)
.addEdge("__start__", "callModel")
.addConditionalEdges("callModel", route);
export const graph = builder.compile();
@@ -1,15 +0,0 @@
import { BaseMessage, BaseMessageLike } from "@langchain/core/messages";
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
/**
* Simple state annotation for the agent
*/
export const StateAnnotation = Annotation.Root({
/**
* Messages track the primary execution state of the agent.
*/
messages: Annotation<BaseMessage[], BaseMessageLike[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -1,14 +0,0 @@
{
"name": "@js-monorepo-example/shared",
"version": "0.0.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}

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