Compare commits

..
Author SHA1 Message Date
Nuno Campos 1b9b0a686e Remove more mentions of async 2025-03-01 17:25:58 -08:00
Nuno Campos 9e02a23682 Rm other mentions of stream_mode=messages 2025-03-01 14:20:15 -08:00
Nuno Campos 5e70e6f307 Rm docs 2025-03-01 14:10:02 -08:00
Nuno Campos eb57c06896 Remove features and dependencies
- rm langchain_core dependency
- replace callbacks w run tree
- rm Runnable dependency
- rm non-state Graph
- rm managed values
- rm entrypoint/task/call
- rm async methods
- rm shallow checkpointer
- rm messages stream mode
- rm debug flag
- rm remote graph
2025-03-01 13:53:27 -08:00
Nuno Campos 9284b57ba0 Remove prebuilt 2025-03-01 10:30:18 -08:00
Nuno Campos 25fea591b5 Remove sqlite 2025-03-01 10:14:10 -08:00
Nuno Campos b9fe53777f Remove cli 2025-03-01 10:13:58 -08:00
Nuno Campos 35c2e8a679 Remove kafka 2025-03-01 10:13:47 -08:00
Nuno Campos e0fb56c6a3 Remove sdks 2025-03-01 10:13:36 -08:00
Nuno Campos 3458a3cecb Remove examples 2025-03-01 10:13:24 -08:00
489 changed files with 22932 additions and 135640 deletions
+29 -52
View File
@@ -1,60 +1,44 @@
name: "\U0001F41B Bug Report"
description: Report a bug in LangGraph. To report a security issue, please instead use the security option (below). For questions, please use the LangChain forum (below).
labels: ["bug"]
type: bug
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the GitHub Discussions.
labels: ["02 Bug Report"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a bug report.
For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
Check these before submitting to see if your issue has already been reported, fixed or if there's another way to solve your problem:
* [Documentation](https://docs.langchain.com/oss/python/langgraph/overview),
* [API Reference Documentation](https://reference.langchain.com/python/),
* [LangChain ChatBot](https://chat.langchain.com/)
* [GitHub search](https://github.com/langchain-ai/langgraph),
* [LangChain Forum](https://forum.langchain.com/),
value: >
Thank you for taking the time to file a bug report.
Use this to report BUGS in LangGraph. For usage questions, feature requests and general design questions, please use [GitHub Discussions](https://github.com/langchain-ai/langgraph/discussions).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
[LangGraph Github Discussions](https://github.com/langchain-ai/langgraph/discussions),
[LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
[LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
[LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
[GitHub search](https://github.com/langchain-ai/langgraph),
- type: checkboxes
id: checks
attributes:
label: Checked other resources
description: Please confirm and check all the following options.
description: Before submitting this issue, please confirm that you have completed all the steps below by checking each option. These steps help ensure your issue is well-defined, relevant, and actionable.
options:
- label: This is a bug, not a usage question.
- label: This is a bug, not a usage question. For questions, please use GitHub Discussions.
required: true
- label: I added a clear and descriptive title that summarizes this issue.
- label: I added a clear and detailed title that summarizes the issue.
required: true
- label: I used the GitHub search to find a similar question and didn't find it.
- label: I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
required: true
- label: I am sure that this is a bug in LangGraph rather than my code.
required: true
- label: The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).
required: true
- label: This is not related to the langchain-community package.
required: true
- label: I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.
- label: I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.
required: true
- type: textarea
id: reproduction
validations:
required: true
attributes:
label: Reproduction Steps / Example Code (Python)
label: Example Code
description: |
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
If a maintainer can copy it, run it, and see it right away, there's a much higher chance that you'll be able to get help.
**Important!**
* Avoid screenshots, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
* Reduce your code to the minimum required to reproduce the issue if possible.
(This will be automatically formatted into code, so no need for backticks.)
render: python
placeholder: |
from langgraph.graph import StateGraph
@@ -63,13 +47,17 @@ body:
chain = StateGraph(list)
chain.invoke('Hello!')
render: python
- type: textarea
id: error
validations:
required: false
attributes:
label: Error Message and Stack Trace (if applicable)
description: |
If you are reporting an error, please copy and paste the full error message and
stack trace.
(This will be automatically formatted into code, so no need for backticks.)
If you are reporting an error, please include the full error message and stack trace.
placeholder: |
Exception + full stack trace
render: shell
- type: textarea
id: description
@@ -90,18 +78,7 @@ body:
attributes:
label: System Info
description: |
Please share your system info with us.
Run the following command in your terminal and paste the output here:
`python -m langchain_core.sys_info`
or if you have an existing python interpreter running:
```python
from langchain_core import sys_info
sys_info.print_sys_info()
```
python -m langchain_core.sys_info
placeholder: |
python -m langchain_core.sys_info
validations:
+12 -12
View File
@@ -1,15 +1,15 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: 💬 LangChain Forum
url: https://forum.langchain.com/
about: General community discussions and support
- name: 📚 LangGraph Documentation
url: https://docs.langchain.com/oss/python/langgraph/overview
about: View the official LangGraph documentation
- name: 📚 API Reference Documentation
url: https://reference.langchain.com/python/
about: View the official LangGraph API reference documentation
- name: 📚 Documentation issue
url: https://github.com/langchain-ai/docs/issues/new?template=02-langgraph.yml
about: Report an issue related to the LangGraph documentation
- name: 🤔 Question or Problem
about: Ask a question or ask about a problem in GitHub Discussions.
url: https://github.com/langchain-ai/langgraph/discussions/categories/q-a
- name: Feature Request
url: https://github.com/langchain-ai/langgraph/discussions/categories/ideas
about: Suggest a feature or an idea
- name: Show and tell
about: Show what you built with LangChain
url: https://github.com/langchain-ai/langgraph/discussions/categories/show-and-tell
- name: Slack
url: https://www.langchain.com/join-community
about: General community discussions
+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: [03 - 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.
+8 -12
View File
@@ -1,29 +1,25 @@
name: 🔒 Privileged
description: You are a LangGraph maintainer, or was asked directly by a maintainer to create an issue here. If not, check the other options.
description: You are a LangChain maintainer, or was asked directly by a maintainer to create an issue here. If not, check the other options.
body:
- type: markdown
attributes:
value: |
Thanks for your interest in LangGraph! 🚀
If you are not a LangGraph maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation on the [LangChain Forum](https://forum.langchain.com/) instead.
You are a LangGraph maintainer if you maintain any of the packages inside of the LangGraph repository
or are a regular contributor to LangGraph with previous merged merged pull requests.
Thanks for your interest in LangChain! 🚀
If you are not a LangChain maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation in a [Question in GitHub Discussions](https://github.com/langchain-ai/langchain/discussions/categories/q-a) instead.
You are a LangChain maintainer if you maintain any of the packages inside of the LangChain repository
or are a regular contributor to LangChain with previous merged merged pull requests.
- type: checkboxes
id: privileged
attributes:
label: Privileged issue
description: Confirm that you are allowed to create an issue here.
options:
- label: I am a LangGraph maintainer, or was asked directly by a LangGraph maintainer to create an issue here.
- label: I am a LangChain maintainer, or was asked directly by a LangChain maintainer to create an issue here.
required: true
- type: textarea
id: content
attributes:
label: Issue Content
description: Add the content of the issue here.
- type: markdown
attributes:
value: |
Community members should **NOT** work on Privileged issues unless these issues have been explicitly marked with a "help-wanted" tag.
-31
View File
@@ -1,31 +0,0 @@
Thank you for contributing to LangGraph! Follow these steps to mark your pull request as ready for review. **If any of these steps are not completed, your PR will not be considered for review.**
- [ ] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
- Examples:
- feat(core): add multi-tenant support
- fix(cli): resolve flag parsing error
- docs(openai): update API usage examples
- Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
- Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres, checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do not include it in the PR.
- [ ] **PR message**: ***Delete this entire checklist*** and replace with
- **Description:** a description of the change. Include a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) if applicable.
- **Issue:** the issue # it fixes, if applicable
- **Dependencies:** any dependencies required for this change
- **Twitter handle:** if your PR gets announced, and you'd like a mention, we'll gladly shout you out!
- [ ] **Add tests and docs**: If you're adding a new integration, you must include:
1. A test for the integration, preferably unit tests that do not rely on network access,
2. An example notebook showing its use. It lives in `docs/docs/integrations` directory.
- [ ] **Lint and test**: Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified. We will not consider a PR unless these three are passing in CI. See [contribution guidelines](https://docs.langchain.com/oss/python/contributing/overview) for more.
Additional guidelines:
- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
+88
View File
@@ -0,0 +1,88 @@
# An action for setting up poetry install with caching.
# Using a custom action since the default action does not
# take poetry install groups into account.
# Action code from:
# https://github.com/actions/setup-python/issues/505#issuecomment-1273013236
name: poetry-install-with-caching
description: Poetry install with support for caching of dependency groups.
inputs:
python-version:
description: Python version, supporting MAJOR.MINOR only
required: true
poetry-version:
description: Poetry version
required: true
cache-key:
description: Cache key to use for manual handling of caching
required: true
runs:
using: composite
steps:
- uses: actions/setup-python@v5
name: Setup python ${{ inputs.python-version }}
id: setup-python
with:
python-version: ${{ inputs.python-version }}
- uses: actions/cache@v3
id: cache-bin-poetry
name: Cache Poetry binary - Python ${{ inputs.python-version }}
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "1"
with:
path: |
/opt/pipx/venvs/poetry
# This step caches the poetry installation, so make sure it's keyed on the poetry version as well.
key: bin-poetry-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-${{ inputs.poetry-version }}
- name: Refresh shell hashtable and fixup softlinks
if: steps.cache-bin-poetry.outputs.cache-hit == 'true'
shell: bash
env:
POETRY_VERSION: ${{ inputs.poetry-version }}
PYTHON_VERSION: ${{ inputs.python-version }}
run: |
set -eux
# Refresh the shell hashtable, to ensure correct `which` output.
hash -r
# `actions/cache@v3` doesn't always seem able to correctly unpack softlinks.
# Delete and recreate the softlinks pipx expects to have.
rm /opt/pipx/venvs/poetry/bin/python
cd /opt/pipx/venvs/poetry/bin
ln -s "$(which "python$PYTHON_VERSION")" python
chmod +x python
cd /opt/pipx_bin/
ln -s /opt/pipx/venvs/poetry/bin/poetry poetry
chmod +x poetry
# Ensure everything got set up correctly.
/opt/pipx/venvs/poetry/bin/python --version
/opt/pipx_bin/poetry --version
- name: Install poetry
if: steps.cache-bin-poetry.outputs.cache-hit != 'true'
shell: bash
env:
POETRY_VERSION: ${{ inputs.poetry-version }}
PYTHON_VERSION: ${{ inputs.python-version }}
# Install poetry using the python version installed by setup-python step.
run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose
- name: Restore pip and poetry cached dependencies
uses: actions/cache@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "4"
with:
path: |
~/.cache/pip
~/.cache/pypoetry/virtualenvs
~/.cache/pypoetry/cache
~/.cache/pypoetry/artifacts
./.venv
key: py-deps-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-poetry-${{ inputs.poetry-version }}-${{ inputs.cache-key }}-${{ hashFiles('./poetry.lock') }}
-111
View File
@@ -1,111 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/checkpoint"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/checkpoint-conformance"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/checkpoint-postgres"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/checkpoint-sqlite"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/cli"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/langgraph"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/prebuilt"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/sdk-py"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/libs/cli/js-examples"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/libs/cli/js-monorepo-example"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
+3 -8
View File
@@ -1,15 +1,10 @@
import ast
import os
from itertools import filterfalse
from typing import Dict, List, Tuple
from typing import List, Tuple
ROOT_PATH = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
CLIENT_PATH = os.path.join(ROOT_PATH, "libs", "sdk-py", "langgraph_sdk", "client.py")
ASYNC_TO_SYNC_METHOD_MAP: Dict[str, str] = {
"aclose": "close",
"__aenter__": "__enter__",
"__aexit__": "__exit__",
}
def get_class_methods(node: ast.ClassDef) -> List[str]:
@@ -27,7 +22,7 @@ def find_classes(tree: ast.AST) -> List[Tuple[str, List[str]]]:
def compare_sync_async_methods(sync_methods: List[str], async_methods: List[str]) -> List[str]:
sync_set = set(sync_methods)
async_set = {ASYNC_TO_SYNC_METHOD_MAP.get(async_method, async_method) for async_method in async_methods}
async_set = set(async_methods)
missing_in_sync = list(async_set - sync_set)
missing_in_async = list(sync_set - async_set)
return missing_in_sync + missing_in_async
@@ -38,7 +33,7 @@ def main():
tree = ast.parse(file.read())
classes = find_classes(tree)
def is_sync(class_spec: Tuple[str, List[str]]) -> bool:
return class_spec[0].startswith("Sync")
+84 -146
View File
@@ -1,164 +1,108 @@
import logging
import asyncio
import json
import os
import pathlib
import sys
import time
from urllib import error, request
import langgraph_cli
import langgraph_cli.config
import langgraph_cli.docker
from langgraph_cli.cli import prepare_args_and_stdin
from langgraph_cli.constants import DEFAULT_PORT
import langgraph_cli.config
from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.progress import Progress
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
from langgraph_cli.constants import DEFAULT_PORT
def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
"""Spin up API with Postgres/Redis via docker compose and wait until ready."""
logger.info("Starting test...")
def test(
config: pathlib.Path,
port: int,
tag: str,
verbose: bool,
):
with Runner() as runner, Progress(message="Pulling...") as set:
# Detect docker/compose capabilities
# check docker available
capabilities = langgraph_cli.docker.check_capabilities(runner)
# Validate config and prepare compose stdin/args using built image
# open config
config_json = langgraph_cli.config.validate_config_file(config)
args, stdin = prepare_args_and_stdin(
capabilities=capabilities,
config_path=config,
config=config_json,
docker_compose=None,
port=port,
watch=False,
debugger_port=None,
debugger_base_url=f"http://127.0.0.1:{port}",
postgres_uri=None,
api_version=None,
image=tag,
base_image=None,
)
# Compose up with wait (implies detach), similar to `langgraph up --wait`
args_up = [*args, "up", "--remove-orphans", "--wait"]
compose_cmd = ["docker", "compose"]
if capabilities.compose_type == "standalone":
compose_cmd = ["docker-compose"]
set("Starting...")
try:
runner.run(
subp_exec(
*compose_cmd,
*args_up,
input=stdin,
verbose=verbose,
)
set("Running...")
args = [
"run",
"--rm",
"-p",
f"{port}:8000",
]
if isinstance(config_json["env"], str):
args.extend(
[
"--env-file",
str(config.parent / config_json["env"]),
]
)
except Exception as e: # noqa: BLE001
# On failure, show diagnostics then ensure clean teardown
sys.stderr.write(f"docker compose up failed: {e}\n")
try:
sys.stderr.write("\n== docker compose ps ==\n")
runner.run(
subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=True)
)
except Exception:
pass
try:
sys.stderr.write("\n== docker compose logs (api) ==\n")
runner.run(
subp_exec(
*compose_cmd,
*args,
"logs",
"langgraph-api",
input=stdin,
verbose=True,
)
)
except Exception:
pass
finally:
try:
runner.run(
subp_exec(
*compose_cmd,
*args,
"down",
"-v",
"--remove-orphans",
input=stdin,
verbose=False,
)
)
finally:
raise
set("")
base_url = f"http://localhost:{port}"
ok_url = f"{base_url}/ok"
logger.info(f"Waiting for {ok_url} to respond with 200...")
deadline = time.time() + 30
last_err: Exception | None = None
while time.time() < deadline:
try:
with request.urlopen(ok_url, timeout=2) as resp:
if resp.status == 200:
sys.stdout.write(
f"""Ready!\n- API: {base_url}\n- /ok: 200 OK\n"""
)
sys.stdout.flush()
break
else:
last_err = RuntimeError(f"Unexpected status: {resp.status}")
logger.error(f"Unexpected status: {resp.status}")
except error.URLError as e:
logger.error(f"URLError: {e}")
last_err = e
except Exception as e: # noqa: BLE001
logger.error(f"Exception: {e}")
last_err = e
time.sleep(0.5)
else:
logger.error("Timeout waiting for /ok to return 200")
# Bring stack down before raising
args_down = [*args, "down", "-v", "--remove-orphans"]
try:
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
verbose=verbose,
)
)
finally:
raise SystemExit(
f"/ok did not return 202 within timeout. Last error: {last_err}"
for k, v in config_json["env"].items():
args.extend(
[
"-e",
f"{k}={v}",
]
)
if capabilities.healthcheck_start_interval:
args.extend(
[
"--health-interval",
"5s",
"--health-retries",
"1",
"--health-start-period",
"10s",
"--health-start-interval",
"1s",
]
)
else:
args.extend(
[
"--health-interval",
"5s",
"--health-retries",
"2",
]
)
_task = None
def on_stdout(line: str):
nonlocal _task
if "GET /ok" in line or "Uvicorn running on" in line:
set("")
sys.stdout.write(
f"""Ready!
- API: http://localhost:{port}
"""
)
sys.stdout.flush()
_task.cancel()
return True
return False
async def subp_exec_task(*args, **kwargs):
nonlocal _task
_task = asyncio.create_task(subp_exec(*args, **kwargs))
await _task
# Clean up: bring compose stack down to free ports for next test
logger.info("Test succeeded. Bringing down compose stack...")
try:
args_down = [*args, "down", "-v", "--remove-orphans"]
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
subp_exec_task(
"docker",
*args,
tag,
verbose=verbose,
on_stdout=on_stdout,
)
)
logger.info("Compose stack down. Finishing...")
except Exception:
logger.exception("Failed to bring down compose stack")
except asyncio.CancelledError:
pass
logger.info("Test finished")
if __name__ == "__main__":
import argparse
@@ -166,12 +110,6 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--tag", type=str)
parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT)
parser.add_argument("-p", "--port", default=DEFAULT_PORT)
args = parser.parse_args()
try:
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
except BaseException:
logger.exception("Test failed")
raise
logger.info("Test execution finished")
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
+37 -101
View File
@@ -2,12 +2,9 @@ name: CLI integration test
on:
workflow_call:
secrets:
LANGSMITH_API_KEY:
required: false
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
jobs:
build:
@@ -16,123 +13,62 @@ jobs:
matrix:
python-version:
- "3.10"
- "3.14"
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
- "3.11"
name: "CLI integration test"
env:
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
defaults:
run:
working-directory: libs/cli
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "libs/cli/**"
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: astral-sh/setup-uv@v7
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: "cli-integration-test"
ignore-nothing-to-cache: true
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: integration-test-cli
- 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 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
run: |
langgraph build -t ${{ matrix.example.tag }}
- name: Test service ${{ matrix.example.name }}
if: ${{ steps.changed-files.outputs.all && env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: ${{ matrix.example.workdir }}
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
# The build-arg isn't used; just testing that we accept other args
langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial"
cp .env.example .envg
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
run: |
# 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
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
# Run the integration test using the built tag
REPO_ROOT=$(git rev-parse --show-toplevel)
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial"
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
run: |
langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial"
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
run: |
langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
- name: Build JS service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
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 && matrix.example.name == 'A' }}
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 && matrix.example.name == 'A' }}
working-directory: libs/cli/python-monorepo-example
run: |
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
- name: Test Python monorepo service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: libs/cli/python-monorepo-example
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
cp apps/agent/.env.example apps/agent/.env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build prerelease reqs service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs
run: |
langgraph build -t langgraph-test-h
- name: Test prerelease reqs service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: libs/cli/examples/graph_prerelease_reqs
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
cp ../.env.example .env
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
echo "Finished starting up 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.8" ]; then
echo "LANGGRAPH_VERSION != 1.0.8; $LANGGRAPH_VERSION"
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" != "1.0.1" ]; then
echo "LANGCHAIN_OPENAI_VERSION != 1.0.1; $LANGCHAIN_OPENAI_VERSION"
exit 1
fi
LANGCHAIN_ANTHROPIC_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-anthropic'); print(v);")
if [ "$LANGCHAIN_ANTHROPIC_VERSION" != "1.0.0a5" ]; then
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.0.0a5; $LANGCHAIN_ANTHROPIC_VERSION"
exit 1
fi
- name: Build and test prerelease reqs fail service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
run: |
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
+42 -14
View File
@@ -8,10 +8,9 @@ on:
type: string
description: "From which folder this pipeline executes"
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
# This env var allows us to get inline annotations when ruff has complaints.
RUFF_OUTPUT_FORMAT: github
@@ -31,34 +30,54 @@ jobs:
- "3.12"
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: astral-sh/setup-uv@v7
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: lint-${{ inputs.working-directory }}
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: lint-${{ inputs.working-directory }}
- name: Check Poetry File
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry check
- name: Check lock file
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry lock --check
- name: Install dependencies
if: steps.changed-files.outputs.all
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
# https://github.com/langchain-ai/langchain/pull/10249/files#diff-935185cd488d015f026dcd9e19616ff62863e8cde8c0bee70318d3ccbca98341
#
# If you change this configuration, make sure to change the `cache-key`
# in the `poetry_setup` action above to stop using the old cache.
# It doesn't matter how you change it, any change will cause a cache-bust.
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group lint
run: poetry install --with dev
- name: Get .mypy_cache to speed up mypy
if: steps.changed-files.outputs.all
uses: actions/cache@v5
uses: actions/cache@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
with:
path: |
${{ inputs.working-directory }}/.mypy_cache
key: mypy-lint-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
key: mypy-lint-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/poetry.lock', inputs.working-directory)) }}
- name: Analysing package code with our lint
if: steps.changed-files.outputs.all
@@ -73,18 +92,27 @@ jobs:
- name: Install test dependencies
if: steps.changed-files.outputs.all
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
# https://github.com/langchain-ai/langchain/pull/10249/files#diff-935185cd488d015f026dcd9e19616ff62863e8cde8c0bee70318d3ccbca98341
#
# If you change this configuration, make sure to change the `cache-key`
# in the `poetry_setup` action above to stop using the old cache.
# It doesn't matter how you change it, any change will cause a cache-bust.
working-directory: ${{ inputs.working-directory }}
run: uv sync --group lint
run: |
poetry install --with dev
- name: Get .mypy_cache_test to speed up mypy
if: steps.changed-files.outputs.all
uses: actions/cache@v5
uses: actions/cache@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
with:
path: |
${{ inputs.working-directory }}/.mypy_cache_test
key: mypy-test-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
key: mypy-test-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/poetry.lock', inputs.working-directory)) }}
- name: Analysing tests with our lint
if: steps.changed-files.outputs.all
+12 -10
View File
@@ -8,8 +8,8 @@ on:
type: string
description: "From which folder this pipeline executes"
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
jobs:
build:
@@ -17,21 +17,21 @@ jobs:
strategy:
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: test-${{ inputs.working-directory }}
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: test-${{ inputs.working-directory }}
- name: Login to Docker Hub
uses: docker/login-action@v3
if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -42,12 +42,14 @@ jobs:
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group test --no-dev
run: |
poetry install --with dev
- name: Run tests
shell: bash
working-directory: ${{ inputs.working-directory }}
run: make test
run: |
make test
- name: Ensure the tests did not create any additional files
shell: bash
+12 -10
View File
@@ -3,8 +3,8 @@ name: test
on:
workflow_call:
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
jobs:
build:
@@ -12,24 +12,24 @@ jobs:
strategy:
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
defaults:
run:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: "test-langgraph"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: test-langgraph
- name: Login to Docker Hub
uses: docker/login-action@v3
if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -39,11 +39,13 @@ jobs:
- name: Install dependencies
shell: bash
run: uv sync --frozen --group test --no-dev
run: |
poetry install --with dev
- name: Run tests
shell: bash
run: make test_parallel
run: |
make test_parallel
- name: Ensure the tests did not create any additional files
shell: bash
+13 -14
View File
@@ -9,13 +9,12 @@ on:
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "1.7.1"
PYTHON_VERSION: "3.10"
permissions:
contents: read
jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
@@ -23,14 +22,14 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python $${ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v7
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: release
# We want to keep this build stage *separate* from the release stage,
# so that there's no sharing of permissions between them.
@@ -44,11 +43,11 @@ jobs:
# > from the publish job.
# https://github.com/pypa/gh-action-pypi-publish#non-goals
- name: Build project for distribution
run: uv build
run: poetry build
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
@@ -58,8 +57,8 @@ jobs:
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
echo pkg-name=$(grep -m 1 "^name = " pyproject.toml | cut -d '"' -f 2)
echo version=$(grep -m 1 "^version = " pyproject.toml | cut -d '"' -f 2)
echo pkg-name="$(poetry version | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT
echo version="$(poetry version --short)" >> $GITHUB_OUTPUT
publish:
needs:
@@ -74,9 +73,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v4
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
@@ -0,0 +1,57 @@
name: test
on:
workflow_call:
env:
POETRY_VERSION: "1.7.1"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.11"
- "3.12"
defaults:
run:
working-directory: libs/scheduler-kafka
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: test-scheduler-kafka
- name: Login to Docker Hub
uses: docker/login-action@v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
- name: Install dependencies
shell: bash
run: |
poetry install --with dev
- name: Run tests
shell: bash
run: |
make test
- name: Ensure the tests did not create any additional files
shell: bash
run: |
set -eu
STATUS="$(git status)"
echo "$STATUS"
# grep will exit non-zero if the target message isn't found,
# and `set -e` above will cause the step to fail.
echo "$STATUS" | grep 'nothing to commit, working tree clean'
+9 -9
View File
@@ -7,8 +7,8 @@ on:
paths:
- "libs/**"
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
jobs:
benchmark:
@@ -17,20 +17,20 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v6
- 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@v7
- name: Set up Python 3.11 + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
enable-cache: true
cache-suffix: "bench"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: bench
- name: Install dependencies
run: uv sync --group test
run: poetry install --with dev
- name: Run benchmarks
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
- name: Save outputs
uses: actions/cache/save@v5
uses: actions/cache/save@v4
with:
key: ${{ runner.os }}-benchmark-baseline-${{ env.SHA }}
path: |
+12 -12
View File
@@ -5,8 +5,8 @@ on:
paths:
- "libs/**"
permissions:
contents: read
env:
POETRY_VERSION: "1.7.1"
jobs:
benchmark:
@@ -15,22 +15,22 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- id: files
name: Get changed files
uses: Ana06/get-changed-files@v2.3.0
with:
format: json
- name: Set up Python 3.11
uses: astral-sh/setup-uv@v7
- name: Set up Python 3.11 + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
enable-cache: true
cache-suffix: "bench"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: bench
- name: Install dependencies
run: uv sync --group test
run: poetry install --with dev
- name: Download baseline
uses: actions/cache/restore@v5
uses: actions/cache/restore@v4
with:
key: ${{ runner.os }}-benchmark-baseline
restore-keys: |
@@ -43,7 +43,7 @@ jobs:
run: |
{
echo 'OUTPUT<<EOF'
make -s benchmark-fast
make -s benchmark
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Compare benchmarks
@@ -53,11 +53,11 @@ jobs:
echo 'OUTPUT<<EOF'
mv out/benchmark-baseline.json out/main.json
mv out/benchmark.json out/changes.json
uv run pyperf compare_to out/main.json out/changes.json --table --group-by-speed
poetry run pyperf compare_to out/main.json out/changes.json --table --group-by-speed
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]
+77 -58
View File
@@ -3,13 +3,9 @@ name: CI
on:
push:
branches:
- main
branches: [main]
pull_request:
permissions:
contents: read
# If another push to the same PR or branch happens while this workflow is still running,
# cancel the earlier run in favor of the next run.
#
@@ -20,14 +16,17 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
POETRY_VERSION: "1.7.1"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -39,11 +38,10 @@ jobs:
- 'libs/checkpoint/**'
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/checkpoint-conformance/**'
- 'libs/scheduler-kafka/**'
- 'libs/prebuilt/**'
deps:
- '**/pyproject.toml'
- '**/uv.lock'
sdk-js:
- 'libs/sdk-js/**'
lint:
needs: changes
@@ -58,10 +56,10 @@ jobs:
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/checkpoint-conformance",
"libs/scheduler-kafka",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -78,11 +76,9 @@ jobs:
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/checkpoint-conformance",
"libs/prebuilt",
"libs/sdk-py",
]
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -91,78 +87,101 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
if: needs.changes.outputs.python == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
test-scheduler-kafka:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/scheduler-kafka"
uses: ./.github/workflows/_test_scheduler_kafka.yml
secrets: inherit
check-sdk-methods:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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
run: python .github/scripts/check_sdk_methods.py
check-schema:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check CLI schema hasn't changed #${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.13"
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"
enable-cache: true
cache-suffix: "schema-check-cli"
- name: Install CLI dependencies
run: |
cd libs/cli
uv sync
- name: Generate schema and check for changes
run: |
cd libs/cli
# Create a temporary copy of the current schema
cp schemas/schema.json schemas/schema.current.json
# Generate new schema
uv run python generate_schema.py
# Compare the new schema with the original
if ! diff -q schemas/schema.json schemas/schema.current.json > /dev/null; then
echo "Error: Langgraph.json configuration schema has changed. Please run 'uv run python generate_schema.py' in the libs/cli directory and commit the changes."
diff schemas/schema.json schemas/schema.current.json
exit 1
fi
echo "Schema check passed - no changes detected"
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
if: needs.changes.outputs.python == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
lint-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js (LTS)
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Run lint
run: yarn lint
- name: Build
run: yarn build
test-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js (LTS)
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Run tests
run: yarn test
ci_success:
name: "CI Success"
needs:
[
lint,
lint-js,
test,
test-langgraph,
check-sdk-methods,
check-schema,
test-scheduler-kafka,
integration-test,
test-js,
]
if: |
always()
+43
View File
@@ -0,0 +1,43 @@
---
name: CI / cd . / make spell_check
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
defaults:
run:
working-directory: docs
jobs:
codespell:
name: (Check for spelling errors)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Dependencies
run: |
pip install toml codespell==2.3.0 jupytext
- name: Extract Ignore Words List
run: |
# Use a Python script to extract the ignore words list from pyproject.toml
python ../.github/workflows/extract_ignored_words_list.py
id: extract_ignore_words
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.md'
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
# We do this to avoid spellchecking cell outputs
- name: Codespell Notebooks
run: make codespell
-49
View File
@@ -1,49 +0,0 @@
name: Deploy Redirects to GitHub Pages
on:
push:
branches:
- main
paths:
- 'docs/**'
- '.github/workflows/deploy-redirects.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Generate redirect files
run: python docs/generate_redirects.py
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: 'docs/_site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+170
View File
@@ -0,0 +1,170 @@
name: Deploy Docs
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
env:
POETRY_VERSION: "1.7.1"
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
working-directory: docs
jobs:
get-changed-files:
runs-on: ubuntu-latest
outputs:
changed-files: ${{ steps.changed-files.outputs.added_modified }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "docs/docs/**"
run-changed-notebooks:
needs: get-changed-files
uses: ./.github/workflows/run_notebooks.yml
secrets: inherit
with:
changed-files: ${{ needs.get-changed-files.outputs.changed-files }}
deploy:
# needs: run-changed-notebooks
runs-on: ubuntu-latest
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.12"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: docs
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
cache: "yarn"
cache-dependency-path: docs/yarn.lock
- name: Install dependencies
run: |
yarn
poetry install --with test --with docs --no-root
poetry run pip install -U \
pytest \
pytest-check-links \
GitPython \
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
# we run this installation only for internal PRs
# as GITHUB_TOKEN is not available for PRs from outside contributors
if [ -n "${GITHUB_TOKEN}" ]; then
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
fi
poetry run jupyter kernelspec list
poetry run python3 -m ipykernel install --user --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
- name: Run unit tests
# Run unit tests on the docs build pipeline
run: make tests
- name: Lint Docs
# This step lints the docs using the existing linting set up.
# It should be very fast and should not require any external services.
run: make lint-docs
- name: Build llms-text
run: make llms-text
- name: Build site
run: make build-docs
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
- name: Check links in notebooks
env:
LANGCHAIN_API_KEY: test
run: |
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ] || ([ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]); then
echo "Running link check on all HTML files matching notebooks in docs directory..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "http://localhost:2024.*" \
--check-links-ignore "http://127.0.0.1:.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
--check-links-ignore "https://www\.uber\.com/.*" \
--check-links-ignore "https://pepy\.tech/.*" \
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
else
echo "Fetching changes from origin/main..."
git fetch origin main
echo "Checking for changed notebook files..."
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
echo "Changed files: ${CHANGED_FILES}"
if [ -n "${CHANGED_FILES}" ]; then
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "http://localhost:2024.*" \
--check-links-ignore "http://127.0.0.1:.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links ${CHANGED_FILES} \
|| ([ $? = 5 ] && exit 0 || exit $?)
else
echo "No notebook files changed."
fi
fi
- name: Configure GitHub Pages
if: github.ref == 'refs/heads/main'
uses: actions/configure-pages@v4
- name: Upload Pages Artifact
# if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: ./docs/site/
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
id: deployment
uses: actions/deploy-pages@v4
@@ -0,0 +1,10 @@
import toml
pyproject_toml = toml.load("pyproject.toml")
# Extract the ignore words list (adjust the key as per your TOML structure)
ignore_words_list = (
pyproject_toml.get("tool", {}).get("codespell", {}).get("ignore-words-list")
)
print(f"::set-output name=ignore_words_list::{ignore_words_list}") # noqa: T201
+49
View File
@@ -0,0 +1,49 @@
name: Check Docs & Links
on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
- cron: "0 5 * * *"
workflow_dispatch:
env:
POETRY_VERSION: "1.7.1"
jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check links in Markdown files
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
folder-path: "docs/"
check-modified-files-only: ${{ github.event_name != 'schedule' }}
file-path: "./README.md"
config-file: "./.markdown-link-check.config.json"
check-readmes-synced:
# This checks that the repo README.md is identical to the libs/langgraph/README.md
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Check README.md is in sync
run: |
if ! diff -q README.md libs/langgraph/README.md >/dev/null; then
echo "README.md is out of sync with libs/langgraph/README.md"
diff -C 3 README.md libs/langgraph/README.md
exit 1
fi
-46
View File
@@ -1,46 +0,0 @@
name: PR Title Lint
permissions:
pull-requests: read
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
lint-pr-title:
runs-on: ubuntu-latest
steps:
- name: Validate PR Title
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
release
scopes: |
checkpoint
checkpoint-postgres
checkpoint-sqlite
cli
langgraph
prebuilt
scheduler-kafka
sdk-py
docs
ci
deps
requireScope: false
ignoreLabels: |
ignore-lint-pr-title
+35 -45
View File
@@ -8,14 +8,13 @@ on:
type: string
default: "libs/langgraph"
permissions:
contents: read
env:
PYTHON_VERSION: "3.11"
POETRY_VERSION: "1.7.1"
jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
@@ -25,14 +24,14 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v7
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: release
# We want to keep this build stage *separate* from the release stage,
# so that there's no sharing of permissions between them.
@@ -46,11 +45,11 @@ jobs:
# > from the publish job.
# https://github.com/pypa/gh-action-pypi-publish#non-goals
- name: Build project for distribution
run: uv build
run: poetry build
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -60,14 +59,8 @@ jobs:
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
PKG_NAME=$(grep -m 1 "^name = " pyproject.toml | cut -d '"' -f 2)
if grep -q 'dynamic.*=.*\[.*"version".*\]' pyproject.toml; then
# handle dynamic versioning
DIR_NAME=$(echo "$PKG_NAME" | tr '-' '_')
VERSION=$(grep -m 1 '^__version__' "${DIR_NAME}/__init__.py" | cut -d '"' -f 2)
else
VERSION=$(grep -m 1 "^version = " pyproject.toml | cut -d '"' -f 2)
fi
PKG_NAME="$(poetry version | cut -d ' ' -f 1)"
VERSION="$(poetry version --short)"
SHORT_PKG_NAME="$(echo "$PKG_NAME" | sed -e 's/langgraph//g' -e 's/-//g')"
if [ -z $SHORT_PKG_NAME ]; then
TAG="$VERSION"
@@ -86,7 +79,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
repository: langchain-ai/langgraph
path: langgraph
@@ -142,9 +135,7 @@ jobs:
needs:
- build
- release-notes
permissions:
contents: read
id-token: write
permissions: write-all
uses: ./.github/workflows/_test_release.yml
with:
working-directory: ${{ inputs.working-directory }}
@@ -157,7 +148,7 @@ jobs:
- test-pypi-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -172,11 +163,11 @@ jobs:
# - The package is published, and it breaks on the missing dependency when
# used in the real world.
- name: Set up Python
uses: astral-sh/setup-uv@v7
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
poetry-version: ${{ env.POETRY_VERSION }}
- name: Import published package
shell: bash
@@ -194,18 +185,18 @@ jobs:
# - attempt install again after 5 seconds if it fails because there is
# sometimes a delay in availability on test pypi
run: |
uv run pip install \
poetry run pip install \
--extra-index-url https://test.pypi.org/simple/ \
"$PKG_NAME==$VERSION" || \
( \
sleep 5 && \
uv run pip install \
poetry run pip install \
--extra-index-url https://test.pypi.org/simple/ \
"$PKG_NAME==$VERSION" \
)
if [[ "$PKG_NAME" == *prebuilt* ]]; then
uv run pip install langgraph
poetry run pip install langgraph
fi
if [[ "$PKG_NAME" == *checkpoint* || "$PKG_NAME" == *prebuilt* ]]; then
@@ -218,10 +209,10 @@ jobs:
IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/_/g)"
fi
uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
poetry run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
- name: Import test dependencies
run: uv sync --group test
run: poetry install --with dev
working-directory: ${{ inputs.working-directory }}
# Overwrite the local version of the package with the test PyPI version.
@@ -232,7 +223,7 @@ jobs:
PKG_NAME: ${{ needs.build.outputs.pkg-name }}
VERSION: ${{ needs.build.outputs.version }}
run: |
uv run pip install \
poetry run pip install \
--extra-index-url https://test.pypi.org/simple/ \
"$PKG_NAME==$VERSION"
@@ -260,16 +251,16 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v7
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: release
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v4
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -301,16 +292,16 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python
uses: astral-sh/setup-uv@v7
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: release
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v4
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -322,6 +313,5 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
generateReleaseNotes: false
tag: ${{needs.build.outputs.tag}}
name: ${{ needs.build.outputs.pkg-name }}==${{ needs.build.outputs.version }}
body: ${{ needs.release-notes.outputs.release-body }}
commit: ${{ github.sha }}
+38
View File
@@ -0,0 +1,38 @@
name: JS Release
on:
workflow_dispatch:
jobs:
publish:
# Disallow publishing from branches that aren't `main`.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v4
# JS Build
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Publish package to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
npm publish
+82
View File
@@ -0,0 +1,82 @@
name: Run notebooks
on:
workflow_dispatch:
workflow_call:
inputs:
changed-files:
required: false
type: string
description: "JSON string of changed files"
schedule:
- cron: '0 13 * * *'
defaults:
run:
working-directory: docs
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
lib-version:
- "development"
- "latest"
steps:
- uses: actions/checkout@v4
- name: Set up Python + Poetry
uses: "./.github/actions/poetry_setup"
with:
python-version: 3.11
poetry-version: 1.7.1
cache-key: test-langgraph-notebooks
- name: Install dependencies
run: |
poetry install --with test
poetry run pip install jupyter
- name: Start services
run: make start-services
- name: Pre-download tiktoken files
run: |
poetry run python _scripts/download_tiktoken.py
- name: Prepare notebooks
run: |
if [ "${{ matrix.lib-version }}" = "development" ]; then
poetry run python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
else
poetry run python _scripts/prepare_notebooks_for_ci.py
fi
- name: Run notebooks
env:
# these won't actually be used because of the VCR cassettes
# but need to set them to avoid triggering getpass()
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
echo "Running all notebooks"
./_scripts/execute_notebooks.sh
else
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | sed 's|^docs/docs/|docs/|' | grep '\.ipynb$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "Running changed notebooks: $CHANGED_FILES"
./_scripts/execute_notebooks.sh $CHANGED_FILES
else
echo "No notebook files changed, skipping execution"
fi
fi
- name: Stop services
run: make stop-services
+29
View File
@@ -0,0 +1,29 @@
name: Check File Size
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
file-size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
- name: Filter by size
# TODO: roll back the web voyager hack
run: |
large_added_files=$(find ${{ steps.changed-files.outputs.added_files }} -maxdepth 0 -size +1M | grep -v "web_voyager" || true)
if [ -n "$large_added_files" ]; then
echo "Large files added: $large_added_files"
echo "# Large files added:" >> $GITHUB_STEP_SUMMARY
echo "$large_added_files" >> $GITHUB_STEP_SUMMARY
exit 1
fi
-45
View File
@@ -1,45 +0,0 @@
name: UV Lock Upgrade
on:
schedule:
# run at midnight every Sunday
- cron: '0 0 * * 0'
# allow manual triggering
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
upgrade-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
# use minimum supported Python version
python-version: "3.10"
enable-cache: true
cache-suffix: "uv-lock-upgrade"
- name: Run uv lock --upgrade in all Python packages
run: make lock-upgrade
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
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`.
This is an automated PR created by the UV Lock Upgrade workflow.
branch: deps/uv-lock-upgrade
delete-branch: true
labels: |
dependencies
+82 -2
View File
@@ -6,6 +6,9 @@ __pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
@@ -51,12 +54,27 @@ coverage.xml
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
docs/docs/_build/
# PyBuilder
target/
@@ -71,9 +89,23 @@ ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
@@ -85,6 +117,16 @@ ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
@@ -96,7 +138,45 @@ dmypy.json
# macOS display setting files
.DS_Store
# Wandb directory
wandb/
# asdf tool versions
.tool-versions
/.ruff_cache/
*.pkl
*.bin
# integration test artifacts
data_map*
\[('_type', 'fake'), ('stop', None)]
# Replit files
*replit*
node_modules
docs/.yarn/
docs/node_modules/
docs/.docusaurus/
docs/.cache-loader/
docs/_dist
docs/api_reference/api_reference.rst
docs/api_reference/experimental_api_reference.rst
docs/api_reference/_build
docs/api_reference/*/
!docs/api_reference/_static/
!docs/api_reference/templates/
!docs/api_reference/themes/
docs/docs_skeleton/build
docs/docs_skeleton/node_modules
docs/docs_skeleton/yarn.lock
# Any new jupyter notebooks
# not intended for the repo
Untitled*.ipynb
Chinook.db
.vercel
.turbo
.editorconfig
.scratch
+4
View File
@@ -0,0 +1,4 @@
{
"aliveStatusCodes": [200, 206, 402],
"ignorePatterns": ["*dcbadge.vercel.app*"]
}
-57
View File
@@ -1,57 +0,0 @@
# AGENTS Instructions
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
- `make format` run code formatters
- `make lint` run the linter
- `make test` execute the test suite
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
```txt
TEST=path/to/test.py make test
```
Other pytest arguments can also be supplied inside the `TEST` variable.
## Libraries
The repository contains several Python and JavaScript/TypeScript libraries.
Below is a high-level overview:
- **checkpoint** base interfaces for LangGraph checkpointers.
- **checkpoint-postgres** Postgres implementation of the checkpoint saver.
- **checkpoint-sqlite** SQLite implementation of the checkpoint saver.
- **cli** official command-line interface for LangGraph.
- **langgraph** core framework for building stateful, multi-actor agents.
- **prebuilt** high-level APIs for creating and running agents and tools.
- **sdk-js** JS/TS SDK for interacting with the LangGraph REST API.
- **sdk-py** Python SDK for the LangGraph Server API.
### Dependency map
The diagram below lists downstream libraries for each production dependency as
declared in that library's `pyproject.toml` (or `package.json`).
```text
checkpoint
├── checkpoint-postgres
├── checkpoint-sqlite
├── prebuilt
└── langgraph
prebuilt
└── langgraph
sdk-py
├── langgraph
└── cli
sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
-57
View File
@@ -1,57 +0,0 @@
# AGENTS Instructions
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
- `make format` run code formatters
- `make lint` run the linter
- `make test` execute the test suite
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
```
TEST=path/to/test.py make test
```
Other pytest arguments can also be supplied inside the `TEST` variable.
## Libraries
The repository contains several Python and JavaScript/TypeScript libraries.
Below is a high-level overview:
- **checkpoint** base interfaces for LangGraph checkpointers.
- **checkpoint-postgres** Postgres implementation of the checkpoint saver.
- **checkpoint-sqlite** SQLite implementation of the checkpoint saver.
- **cli** official command-line interface for LangGraph.
- **langgraph** core framework for building stateful, multi-actor agents.
- **prebuilt** high-level APIs for creating and running agents and tools.
- **sdk-js** JS/TS SDK for interacting with the LangGraph REST API.
- **sdk-py** Python SDK for the LangGraph Server API.
### Dependency map
The diagram below lists downstream libraries for each production dependency as
declared in that library's `pyproject.toml` (or `package.json`).
```text
checkpoint
├── checkpoint-postgres
├── checkpoint-sqlite
├── prebuilt
└── langgraph
prebuilt
└── langgraph
sdk-py
├── langgraph
└── cli
sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
+293
View File
@@ -0,0 +1,293 @@
# Contributing to LangGraph
Thank you for being interested in contributing to LangGraph!
## General guidelines
Here are some things to keep in mind for all types of contributions:
- Follow the ["fork and pull request"](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) workflow.
- Fill out the checked-in pull request template when opening pull requests. Note related issues and tag relevant maintainers.
- Ensure your PR passes formatting, linting, and testing checks before requesting a review.
- If you would like comments or feedback, please open an issue or discussion and tag a maintainer.
- Backwards compatibility is key. Your changes must not be breaking, except in case of critical bug and security fixes.
- Look for duplicate PRs or issues that have already been opened before opening a new one.
- Keep scope as isolated as possible. As a general rule, your changes should not affect more than one package at a time.
### Bugfixes
For bug fixes, please open up an issue before proposing a fix to ensure the proposal properly addresses the underlying problem. In general, bug fixes should all have an accompanying unit test that fails before the fix.
### New features
For new features, please start a new [discussion](https://github.com/langchain-ai/langgraph/discussions), where the maintainers will help with scoping out the necessary changes.
## Contribute Documentation
Documentation is a vital part of LangGraph. We welcome both new documentation for new features and
community improvements to our current documentation. Please read the resources below before getting started:
- [Documentation style guide](#documentation-style-guide)
- [Documentation setup](#setup)
## Documentation Style Guide
As LangGraph continues to grow, the surface area of documentation required to cover it continues to grow too.
This page provides guidelines for anyone writing documentation for LangGraph, as well as some of our philosophies around organization and structure.
## Philosophy
LangGraph's documentation follows the [Diataxis framework](https://diataxis.fr).
Under this framework, all documentation falls under one of four categories: [Tutorials](#tutorials),
[How-to guides](#how-to-guides),
[References](#references), and [Explanations (aka conceptual guides)](#conceptual-guide).
### Tutorials
Tutorials are lessons that take the reader through a practical activity. Their purpose is to help the user
gain understanding of concepts and how they interact by showing one way to achieve some goal in a hands-on way.
They should **avoid** giving
multiple permutations of ways to achieve that goal in-depth. Choice is burdensome. Instead, they should guide a new user through a recommended path to accomplishing a concrete goal. While the end result of a tutorial does not necessarily need to
be completely production-ready, it should be useful and practically satisfy the goal that you clearly stated in the tutorial's introduction.
To quote the Diataxis website:
> A tutorial serves the users *acquisition* of skills and knowledge - their study. Its purpose is not to help the user get something done, but to help them learn.
In LangGraph, these are often higher level guides that show off end-to-end use cases.
Some examples include:
- [Build a Customer Support Bot](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/)
- [Build a SQL Agent](https://langchain-ai.github.io/langgraph/tutorials/sql-agent/)
Here are some high-level tips on writing a good tutorial:
- Focus on guiding the user to get something done, but keep in mind the end-goal is more to impart principles than to create a perfect production system.
- Be specific, not abstract and follow one path.
- No need to go deeply into alternative approaches, but its ok to reference them, ideally with a link to an appropriate how-to guide.
- Get "a point on the board" as soon as possible - something the user can run that outputs something.
- You can iterate and expand afterwards.
- Try to frequently checkpoint at given steps where the user can run code and see progress.
- Focus on results, not technical explanation.
- Crosslink heavily to appropriate conceptual/reference pages
- The first time you mention a LangGraph concept, use its full name (e.g. "human-in-the-loop"), and link to its conceptual/other documentation page.
- It's also helpful to add a prerequisite callout that links to any pages with necessary background information.
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as related how-to guides.
- Use phrases like "Next we can run X & Y. We will expect Z.". Then afterwards, use language like "Notice Z" that recalls our expectations and directs the reader's attention to the topic we are trying to teach.
- Do not shy away from repetition.
### How-to guides
A how-to guide, as the name implies, demonstrates how to do something discrete and specific.
It should assume that the user is already familiar with underlying concepts, and is trying to solve an immediate problem, but
should still give some background or list the scenarios where the information contained within can be relevant.
They can and should discuss alternatives if one approach may be better than another in certain cases.
To quote the Diataxis website:
> A how-to guide serves the work of the already-competent user, whom you can assume to know what they want to do, and to be able to follow your instructions correctly.
Some examples include:
- [How to add persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/)
- [How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/)
Here are some high-level tips on writing a good how-to guide:
- Clearly explain what you are guiding the user through at the start
- Assume higher intent than a tutorial and show what the user needs to do to get that task done
- Assume familiarity of concepts, but explain why suggested actions are helpful
- Crosslink heavily to conceptual/reference pages
- Discuss alternatives and responses to real-world tradeoffs that may arise when solving a problem
- Use lots of example code, ideally within complete code blocks that the reader can copy and run.
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as other related how-to guides
### Conceptual guides
LangGraph's conceptual guides fall under the **Explanation** quadrant of Diataxis. They should cover LangChain terms and concepts
in a more abstract way than how-to guides or tutorials, and should be geared towards curious users interested in
gaining a deeper understanding of the framework. Try to avoid excessively large code examples. The goal here is to
impart perspective to the user rather than to finish a practical project. These guides should cover **why** things work they way they do.
To quote the Diataxis website:
> The perspective of explanation is higher and wider than that of the other types. It does not take the users eye-level view, as in a how-to guide, or a close-up view of the machinery, like reference material. Its scope in each case is a topic - “an area of knowledge”, that somehow has to be bounded in a reasonable, meaningful way.
Some examples include:
- [What does it mean to be agentic?](https://langchain-ai.github.io/langgraph/concepts/high_level/)
- [Tool calling](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#tool-calling)
Here are some high-level tips on writing a good conceptual guide:
- Explain design decisions. Why does concept X exist and why was it designed this way?
- Use analogies and reference other concepts and alternatives
- Avoid blending in too much reference content
- You can and should reference content covered in other guides, but make sure to link to them
### References
References contain detailed, low-level information that describes exactly what functionality exists and how to use it.
In LangGraph, this is mainly our API reference pages, which are populated from docstrings within code.
References pages are generally not read end-to-end, but are consulted as necessary when a user needs to know
how to use something specific.
To quote the Diataxis website:
> The only purpose of a reference guide is to describe, as succinctly as possible, and in an orderly way. Whereas the content of tutorials and how-to guides are led by needs of the user, reference material is led by the product it describes.
Many of the reference pages in LangChain are automatically generated from code,
but here are some high-level tips on writing a good docstring:
- Be concise
- Discuss special cases and deviations from a user's expectations
- Go into detail on required inputs and outputs
- Light details on when one might use the feature are fine, but in-depth details belong in other sections.
Each category serves a distinct purpose and requires a specific approach to writing and structuring the content.
## General guidelines
Here are some other guidelines you should think about when writing and organizing documentation.
We generally do not merge new tutorials from outside contributors without an actue need.
We welcome updates as well as new integration docs, how-tos, and references.
### Avoid duplication
Multiple pages that cover the same material in depth are difficult to maintain and cause confusion. There should
be only one (very rarely two), canonical pages for a given concept or feature. Instead, you should link to other guides.
### Link to other sections
Because sections of the docs do not exist in a vacuum, it is important to link to other sections as often as possible
to allow a developer to learn more about an unfamiliar topic inline.
This includes linking to the API references as well as conceptual sections!
### Be concise
In general, take a less-is-more approach. If a section with a good explanation of a concept already exists, you should link to it rather than
re-explain it, unless the concept you are documenting presents some new wrinkle.
Be concise, including in code samples.
### General style
- Use active voice and present tense whenever possible
- Use examples and code snippets to illustrate concepts and usage
- Use appropriate header levels (`#`, `##`, `###`, etc.) to organize the content hierarchically
- Use fewer cells with more code to make copy/paste easier
- Use bullet points and numbered lists to break down information into easily digestible chunks
- Use tables (especially for **Reference** sections) and diagrams often to present information visually
- Include the table of contents for longer documentation pages to help readers navigate the content, but hide it for shorter pages
## Setup
LangChain documentation consists of two components:
1. Main Documentation: Hosted at [https://langchain-ai.github.io](https://langchain-ai.github.io/langgraph/),
this comprehensive resource serves as the primary user-facing documentation.
It covers a wide array of topics, including tutorials, use cases, integrations,
and more, offering extensive guidance on building with LangGraph.
The content for this documentation lives in the `/docs` directory of the monorepo.
2. In-code Documentation: This is documentation of the codebase itself, which is also
used to generate the externally facing [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/).
The content for the API reference is autogenerated by scanning the docstrings in the codebase. For this reason we ask that developers document their code well.
We appreciate all contributions to the documentation, whether it be fixing a typo,
adding a new tutorial or example and whether it be in the main documentation or the API Reference.
### 📜 Main Documentation
The content for the main documentation is located in the `/docs` directory of the monorepo.
The documentation is written using a combination of ipython notebooks (`.ipynb` files)
and markdown (`.md` files). The notebooks are converted to markdown
and then built using [MkDocs](https://www.mkdocs.org/).
Feel free to make contributions to the main documentation! 🥰
After modifying the documentation:
1. Run the linting and formatting commands (see below) to ensure that the documentation is well-formatted and free of errors.
2. Optionally build the documentation locally to verify that the changes look good.
3. Make a pull request with the changes.
### ⚒️ Linting and Building Documentation Locally
After writing up the documentation, you may want to lint and build the documentation
locally to ensure that it looks good and is free of errors.
If you're unable to build it locally that's okay as well, as you will be able to
see a preview of the documentation on the pull request page.
From the **monorepo root**, run the following command to install the dependencies:
```bash
poetry install --with docs --no-root
```
#### Building
The code that builds the documentation is located in the `/docs` directory of the monorepo.
Before building the documentation, it is always a good idea to clean the build directory:
```bash
make clean-docs
```
You can build and preview the documentation as outlined below:
```bash
make serve-docs
```
#### Linting
The documentation is linted from the **monorepo root**. To lint it, run the following from there:
```bash
make spellcheck
```
### In-code Documentation
The in-code documentation is autogenerated from docstrings.
For the API reference to be useful, the codebase must be well-documented. This means that all functions, classes, and methods should have a docstring that explains what they do, what the arguments are, and what the return value is. This is a good practice in general, but it is especially important for LangChain because the API reference is the primary resource for developers to understand how to use the codebase.
We generally follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
Here is an example of a well-documented function:
```python
def my_function(arg1: int, arg2: str) -> float:
"""This is a short description of the function. (It should be a single sentence.)
This is a longer description of the function. It should explain what
the function does, what the arguments are, and what the return value is.
It should wrap at 88 characters.
Examples:
This is a section for examples of how to use the function.
.. code-block:: python
my_function(1, "hello")
Args:
arg1: This is a description of arg1. We do not need to specify the type since
it is already specified in the function signature.
arg2: This is a description of arg2.
Returns:
This is a description of the return value.
"""
return 3.14
```
-68
View File
@@ -1,68 +0,0 @@
# Define the directories containing projects
LIBS_DIRS := $(wildcard libs/*)
# Default target
.PHONY: all
all: lint format lock test
# Install dependencies for all projects
.PHONY: install
install:
@echo "Creating virtual environment..."
@uv venv
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/pyproject.toml ]; then \
echo "Installing dependencies for $$dir"; \
uv pip install -e $$dir; \
fi; \
done
# Lint all projects
.PHONY: lint
lint:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running lint in $$dir"; \
$(MAKE) -C $$dir lint; \
fi; \
done
# Format all projects
.PHONY: format
format:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running format in $$dir"; \
$(MAKE) -C $$dir format; \
fi; \
done
# Lock all projects
.PHONY: lock
lock:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running lock in $$dir"; \
(cd $$dir && uv lock); \
fi; \
done
# Lock all projects and upgrade dependencies
.PHONY: lock-upgrade
lock-upgrade:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running lock-upgrade in $$dir"; \
(cd $$dir && uv lock --upgrade); \
fi; \
done
# Test all projects
.PHONY: test
test:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running test in $$dir"; \
$(MAKE) -C $$dir test; \
fi; \
done
+313 -65
View File
@@ -1,91 +1,339 @@
<picture class="github-only">
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
</picture>
# 🦜🕸️LangGraph
<div>
<br>
</div>
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
![Version](https://img.shields.io/pypi/v/langgraph)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://docs.langchain.com/oss/python/langgraph/overview)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
Trusted by companies shaping the future of agents including Klarna, Replit, Elastic, and more LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
⚡ Building language agents as graphs ⚡
## Get started
> [!NOTE]
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
Install LangGraph:
## Overview
```
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
### Why use LangGraph?
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
- **Background runs**: Runs agents asynchronously in the background
- **Support for long running agents**: Infrastructure that can handle long running processes
- **[Double texting](https://langchain-ai.github.io/langgraph/concepts/double_texting)**: Handle the case where you get two messages from the user before the agent can respond
- **Handle burstiness**: Task queue for ensuring requests are handled consistently without loss, even under heavy loads
## Installation
```shell
pip install -U langgraph
```
Create a simple workflow:
## Example
```python
from langgraph.graph import START, StateGraph
from typing_extensions import TypedDict
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
class State(TypedDict):
text: str
def node_a(state: State) -> dict:
return {"text": state["text"] + "a"}
def node_b(state: State) -> dict:
return {"text": state["text"] + "b"}
graph = StateGraph(State)
graph.add_node("node_a", node_a)
graph.add_node("node_b", node_b)
graph.add_edge(START, "node_a")
graph.add_edge("node_a", "node_b")
print(graph.compile().invoke({"text": ""}))
# {'text': 'ab'}
```shell
pip install langchain-anthropic
```
Get started with the [LangGraph Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart).
```shell
export ANTHROPIC_API_KEY=sk-...
```
To quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).
Optionally, we can set up [LangSmith](https://docs.smith.langchain.com/) for best-in-class observability.
## Core benefits
```shell
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_sk_...
```
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
<details open>
<summary>High-level implementation</summary>
## LangGraphs ecosystem
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://docs.langchain.com/oss/javascript/langgraph/overview).
tools = [search]
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
## Additional resources
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
- [Guides](https://docs.langchain.com/oss/python/langgraph/overview): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): 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.
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
- [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale.
app = create_react_agent(model, tools, checkpointer=checkpointer)
## Acknowledgements
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
> LangGraph is a **low-level** framework that allows you to implement any custom agent
architectures. Click on the low-level implementation below to see how to implement a
tool-calling agent from scratch.
<details>
<summary>Low-level implementation</summary>
```python
from typing import Literal
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
tools = [search]
tool_node = ToolNode(tools)
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
# Define the function that determines whether to continue or not
def should_continue(state: MessagesState) -> Literal["tools", END]:
messages = state['messages']
last_message = messages[-1]
# If the LLM makes a tool call, then we route to the "tools" node
if last_message.tool_calls:
return "tools"
# Otherwise, we stop (reply to the user)
return END
# Define the function that calls the model
def call_model(state: MessagesState):
messages = state['messages']
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define a new graph
workflow = StateGraph(MessagesState)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.add_edge(START, "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,
)
# 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("tools", 'agent')
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable.
# Note that we're (optionally) passing the memory when compiling the graph
app = workflow.compile(checkpointer=checkpointer)
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<b>Step-by-step Breakdown</b>:
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
</li>
</ul>
</details>
<details>
<summary>Initialize graph with state.</summary>
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
<details>
<summary>Define graph nodes.</summary>
There are two main nodes we need:
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
<details>
<summary>Define entry point and graph edges.</summary>
First, we need to set the entry point for graph execution - <code>agent</code> node.
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
<details>
<summary>Compile the graph.</summary>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
<details>
<summary>Execute the graph.</summary>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Resources
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
## Contributing
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
-1
View File
@@ -1 +0,0 @@
_site/
-142
View File
@@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""
Generate HTML redirect files from redirects.json.
Usage:
python generate_redirects.py
This script reads redirects.json and generates individual HTML files
for each redirect path. Each HTML file uses meta refresh (0 delay)
which is SEO-friendly and treated similarly to 301 redirects by Google.
To add new redirects, simply edit redirects.json and re-run this script.
"""
import json
import os
from pathlib import Path
# Default fallback URL for any path not in the redirect map
DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview"
HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<link rel="canonical" href="{url}">
<meta name="robots" content="noindex">
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
<meta http-equiv="refresh" content="0; url={url}">
</head>
<body>
Redirecting...
</body>
</html>
"""
ROOT_HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting to LangGraph Documentation</title>
<link rel="canonical" href="{url}">
<meta name="robots" content="noindex">
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
<meta http-equiv="refresh" content="0; url={url}">
</head>
<body>
<h1>Documentation has moved</h1>
<p>The LangGraph documentation has moved to <a href="{url}">docs.langchain.com</a>.</p>
<p>Redirecting you now...</p>
</body>
</html>
"""
CATCHALL_404_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting to LangGraph Documentation</title>
<link rel="canonical" href="{default_url}">
<meta name="robots" content="noindex">
<script>
// Catchall redirect for any unmapped paths
window.location.replace("{default_url}");
</script>
<meta http-equiv="refresh" content="0; url={default_url}">
</head>
<body>
<h1>Documentation has moved</h1>
<p>The LangGraph documentation has moved to <a href="{default_url}">docs.langchain.com</a>.</p>
<p>Redirecting you now...</p>
</body>
</html>
"""
def generate_redirects():
script_dir = Path(__file__).parent
output_dir = script_dir / "_site"
# Load redirects
with open(script_dir / "redirects.json") as f:
redirects = json.load(f)
# Clean output directory
if output_dir.exists():
import shutil
shutil.rmtree(output_dir)
output_dir.mkdir(parents=True)
# Generate individual HTML files for each redirect
for old_path, new_url in redirects.items():
# Remove leading slash and create directory structure
path = old_path.lstrip("/")
# Check if path has a file extension (e.g., .txt, .xml)
# If so, create the file directly instead of a directory with index.html
path_obj = Path(path)
has_extension = path_obj.suffix and len(path_obj.suffix) <= 5
if not path:
html_path = output_dir / "index.html"
elif has_extension:
# For files with extensions, create the file directly
html_path = output_dir / path
else:
# For directory-style URLs, create index.html inside
html_path = output_dir / path / "index.html"
# Create parent directories
html_path.parent.mkdir(parents=True, exist_ok=True)
# Write the redirect HTML
html_path.write_text(HTML_TEMPLATE.format(url=new_url))
print(f"Created: {html_path}")
# Create root index.html
root_index = output_dir / "index.html"
if not root_index.exists():
root_index.write_text(ROOT_HTML_TEMPLATE.format(url=DEFAULT_REDIRECT))
print(f"Created: {root_index}")
# Create 404.html for catchall
catchall_404 = output_dir / "404.html"
catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT))
print(f"Created: {catchall_404}")
# Copy static files (like llms.txt) that can't be redirected via HTML
static_files = ["llms.txt"]
for static_file in static_files:
src = script_dir / static_file
if src.exists():
dst = output_dir / static_file
dst.write_text(src.read_text())
print(f"Copied: {dst}")
print(f"\nGenerated {len(redirects)} redirect files in {output_dir}")
if __name__ == "__main__":
generate_redirects()
-35
View File
@@ -1,35 +0,0 @@
# LangGraph
LangGraph documentation has moved to docs.langchain.com.
## Overview
- [LangGraph Overview](https://docs.langchain.com/oss/python/langgraph/overview): Introduction to LangGraph, a library for building stateful, multi-actor applications with LLMs.
- [Why LangGraph?](https://docs.langchain.com/oss/python/langgraph/why-langgraph): Motivation for LangGraph and its key features.
## Core Concepts
- [Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api): Learn how to define state, create nodes, and connect them with edges.
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming): Stream outputs from your graph for better UX.
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence): Add memory and checkpointing to your graphs.
- [Add Memory](https://docs.langchain.com/oss/python/langgraph/add-memory): Implement short-term and long-term memory.
- [Workflows & Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents): Build agents and workflows with LangGraph.
## How-To Guides
- [Use Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs): Compose graphs using subgraphs.
- [Observability](https://docs.langchain.com/oss/python/langgraph/observability): Add tracing and debugging to your graphs.
- [Common Errors](https://docs.langchain.com/oss/python/langgraph/common-errors): Troubleshoot common LangGraph errors.
## Tutorials
- [Agentic RAG](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Build an agentic RAG system with LangGraph.
- [SQL Agent](https://docs.langchain.com/oss/python/langgraph/sql-agent): Create a SQL agent with LangGraph.
## Reference
- [API Reference](https://reference.langchain.com/python/langgraph/): Complete API documentation for LangGraph.
## LangGraph Platform
For deploying LangGraph applications in production, see the [LangSmith documentation](https://docs.langchain.com/langsmith/agent-server).
-296
View File
@@ -1,296 +0,0 @@
{
"/how-tos/stream-values": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/stream-updates": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/streaming-content": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/stream-multiple": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/streaming-tokens-without-langchain": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/streaming-from-final-node": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/streaming-events-from-within-tools-without-langchain": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/state-reducers": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-and-update-state",
"/how-tos/sequence": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-a-sequence-of-steps",
"/how-tos/branching": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-branches",
"/how-tos/recursion-limit": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-and-control-loops",
"/how-tos/visualization": "https://docs.langchain.com/oss/python/langgraph/graph-api#visualize-your-graph",
"/how-tos/input_output_schema": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-input-and-output-schemas",
"/how-tos/pass_private_state": "https://docs.langchain.com/oss/python/langgraph/graph-api#pass-private-state-between-nodes",
"/how-tos/state-model": "https://docs.langchain.com/oss/python/langgraph/graph-api#use-pydantic-models-for-graph-state",
"/how-tos/map-reduce": "https://docs.langchain.com/oss/python/langgraph/graph-api#map-reduce-and-the-send-api",
"/how-tos/command": "https://docs.langchain.com/oss/python/langgraph/graph-api#combine-control-flow-and-state-updates-with-command",
"/how-tos/configuration": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-runtime-configuration",
"/how-tos/node-retries": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-retry-policies",
"/how-tos/return-when-recursion-limit-hits": "https://docs.langchain.com/oss/python/langgraph/graph-api#impose-a-recursion-limit",
"/how-tos/async": "https://docs.langchain.com/oss/python/langgraph/graph-api#async",
"/how-tos/memory/manage-conversation-history": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/memory/delete-messages": "https://docs.langchain.com/oss/python/langgraph/add-memory#delete-messages",
"/how-tos/memory/add-summary-conversation-history": "https://docs.langchain.com/oss/python/langgraph/add-memory#summarize-messages",
"/how-tos/memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/agents/memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/subgraph-transform-state": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#different-state-schemas",
"/how-tos/subgraphs-manage-state": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#add-persistence",
"/how-tos/persistence_postgres": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
"/how-tos/persistence_mongodb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
"/how-tos/persistence_redis": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
"/how-tos/subgraph-persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-with-subgraphs",
"/how-tos/cross-thread-persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
"/cloud/how-tos/copy_threads": "https://docs.langchain.com/langsmith/use-threads",
"/cloud/how-tos/check-thread-status": "https://docs.langchain.com/langsmith/use-threads",
"/cloud/concepts/threads": "https://docs.langchain.com/oss/python/langgraph/persistence#threads",
"/how-tos/persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/tool-calling-errors": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/how-tos/pass-config-to-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/how-tos/pass-run-time-values-to-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/how-tos/update-state-from-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/agents/tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/how-tos/agent-handoffs": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/multi-agent-network": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/multi-agent-multi-turn-convo": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/cloud/index": "https://docs.langchain.com/oss/python/langgraph/overview",
"/cloud/how-tos/index": "https://docs.langchain.com/langsmith/home",
"/cloud/concepts/api": "https://docs.langchain.com/langsmith/agent-server",
"/cloud/concepts/cloud": "https://docs.langchain.com/langsmith/cloud",
"/cloud/faq/studio": "https://docs.langchain.com/langsmith/studio",
"/cloud/how-tos/human_in_the_loop_edit_state": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
"/cloud/how-tos/human_in_the_loop_user_input": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
"/concepts/platform_architecture": "https://docs.langchain.com/langsmith/cloud#architecture",
"/cloud/how-tos/stream_values": "https://docs.langchain.com/langsmith/streaming",
"/cloud/how-tos/stream_updates": "https://docs.langchain.com/langsmith/streaming",
"/cloud/how-tos/stream_messages": "https://docs.langchain.com/langsmith/streaming",
"/cloud/how-tos/stream_events": "https://docs.langchain.com/langsmith/streaming",
"/cloud/how-tos/stream_debug": "https://docs.langchain.com/langsmith/streaming",
"/cloud/how-tos/stream_multiple": "https://docs.langchain.com/langsmith/streaming",
"/cloud/concepts/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/agents/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/create-react-agent": "https://docs.langchain.com/oss/python/langchain/agents#basic-configuration",
"/how-tos/create-react-agent-memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/create-react-agent-system-prompt": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/create-react-agent-structured-output": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
"/prebuilt": "https://docs.langchain.com/oss/python/langchain/agents",
"/reference/prebuilt": "https://reference.langchain.com/python/langgraph/agents/",
"/concepts/high_level": "https://docs.langchain.com/oss/python/langgraph/overview",
"/concepts/index": "https://docs.langchain.com/oss/python/langgraph/overview",
"/concepts/v0-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/how-tos/index": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/introduction": "https://docs.langchain.com/oss/python/langgraph/overview",
"/agents/deployment": "https://docs.langchain.com/oss/python/langgraph/local-server",
"/how-tos/deploy-self-hosted": "https://docs.langchain.com/langsmith/platform-setup",
"/concepts/self_hosted": "https://docs.langchain.com/langsmith/platform-setup",
"/tutorials/deployment": "https://docs.langchain.com/langsmith/deployments",
"/cloud/how-tos/assistant_versioning": "https://docs.langchain.com/langsmith/configuration-cloud",
"/cloud/concepts/runs": "https://docs.langchain.com/langsmith/assistants#execution",
"/how-tos/wait-user-input-functional": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"/how-tos/review-tool-calls-functional": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"/how-tos/create-react-agent-hitl": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/agents/human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/how-tos/human_in_the_loop/dynamic_breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/concepts/breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/how-tos/human_in_the_loop/breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/cloud/how-tos/human_in_the_loop_breakpoint": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
"/how-tos/human_in_the_loop/edit-graph-state": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
"/examples/index": "https://docs.langchain.com/oss/python/langgraph/case-studies",
"/guides/index": "https://docs.langchain.com/oss/python/langchain/overview",
"/tutorials/index": "https://docs.langchain.com/oss/python/learn",
"/llms-txt-overview": "https://docs.langchain.com/llms.txt",
"/tutorials/rag/langgraph_adaptive_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/multi_agent/multi-agent-collaboration": "https://docs.langchain.com/oss/python/langchain/multi-agent",
"/how-tos/create-react-agent-manage-message-history": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/many-tools": "https://docs.langchain.com/oss/python/langchain/tools",
"/tutorials/customer-support/customer-support": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/how-tos/react-agent-structured-output": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
"/tutorials/code_assistant/langgraph_code_assistant": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/multi_agent/hierarchical_agent_teams": "https://docs.langchain.com/oss/python/langchain/supervisor",
"/tutorials/auth/getting_started": "https://docs.langchain.com/langsmith/auth",
"/tutorials/auth/resource_auth": "https://docs.langchain.com/langsmith/resource-auth",
"/tutorials/auth/add_auth_server": "https://docs.langchain.com/langsmith/add-auth-server",
"/how-tos/use-remote-graph": "https://docs.langchain.com/langsmith/use-remote-graph",
"/how-tos/autogen-integration": "https://docs.langchain.com/langsmith/autogen-integration",
"/how-tos/human_in_the_loop/wait-user-input": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/cloud/how-tos/use_stream_react": "https://docs.langchain.com/langsmith/use-stream-react",
"/cloud/how-tos/generative_ui_react": "https://docs.langchain.com/langsmith/generative-ui-react",
"/concepts/langgraph_platform": "https://docs.langchain.com/langsmith/home",
"/concepts/langgraph_components": "https://docs.langchain.com/langsmith/components",
"/concepts/langgraph_server": "https://docs.langchain.com/langsmith/agent-server",
"/concepts/langgraph_data_plane": "https://docs.langchain.com/langsmith/data-plane",
"/concepts/langgraph_control_plane": "https://docs.langchain.com/langsmith/control-plane",
"/concepts/langgraph_cli": "https://docs.langchain.com/langsmith/cli",
"/concepts/langgraph_studio": "https://docs.langchain.com/langsmith/studio",
"/cloud/how-tos/studio/quick_start": "https://docs.langchain.com/langsmith/quick-start-studio",
"/cloud/how-tos/invoke_studio": "https://docs.langchain.com/langsmith/use-studio",
"/cloud/how-tos/studio/manage_assistants": "https://docs.langchain.com/langsmith/use-studio",
"/cloud/how-tos/threads_studio": "https://docs.langchain.com/langsmith/use-threads",
"/cloud/how-tos/iterate_graph_studio": "https://docs.langchain.com/langsmith/use-studio",
"/cloud/how-tos/studio/run_evals": "https://docs.langchain.com/langsmith/observability",
"/cloud/how-tos/clone_traces_studio": "https://docs.langchain.com/langsmith/observability",
"/cloud/how-tos/datasets_studio": "https://docs.langchain.com/langsmith/use-studio",
"/concepts/sdk": "https://docs.langchain.com/langsmith/sdk",
"/concepts/plans": "https://docs.langchain.com/langsmith/home",
"/concepts/application_structure": "https://docs.langchain.com/langsmith/application-structure",
"/concepts/scalability_and_resilience": "https://docs.langchain.com/langsmith/scalability-and-resilience",
"/concepts/auth": "https://docs.langchain.com/langsmith/auth",
"/how-tos/auth/custom_auth": "https://docs.langchain.com/langsmith/custom-auth",
"/how-tos/auth/openapi_security": "https://docs.langchain.com/langsmith/openapi-security",
"/concepts/assistants": "https://docs.langchain.com/langsmith/assistants",
"/cloud/how-tos/configuration_cloud": "https://docs.langchain.com/langsmith/configuration-cloud",
"/cloud/how-tos/use_threads": "https://docs.langchain.com/langsmith/use-threads",
"/cloud/how-tos/background_run": "https://docs.langchain.com/langsmith/background-run",
"/cloud/how-tos/same-thread": "https://docs.langchain.com/langsmith/same-thread",
"/cloud/how-tos/stateless_runs": "https://docs.langchain.com/langsmith/stateless-runs",
"/cloud/how-tos/configurable_headers": "https://docs.langchain.com/langsmith/configurable-headers",
"/concepts/double_texting": "https://docs.langchain.com/langsmith/double-texting",
"/cloud/how-tos/interrupt_concurrent": "https://docs.langchain.com/langsmith/interrupt-concurrent",
"/cloud/how-tos/rollback_concurrent": "https://docs.langchain.com/langsmith/rollback-concurrent",
"/cloud/how-tos/reject_concurrent": "https://docs.langchain.com/langsmith/reject-concurrent",
"/cloud/how-tos/enqueue_concurrent": "https://docs.langchain.com/langsmith/enqueue-concurrent",
"/cloud/concepts/webhooks": "https://docs.langchain.com/langsmith/use-webhooks",
"/cloud/how-tos/webhooks": "https://docs.langchain.com/langsmith/use-webhooks",
"/cloud/concepts/cron_jobs": "https://docs.langchain.com/langsmith/cron-jobs",
"/cloud/how-tos/cron_jobs": "https://docs.langchain.com/langsmith/cron-jobs",
"/how-tos/http/custom_lifespan": "https://docs.langchain.com/langsmith/custom-lifespan",
"/how-tos/http/custom_middleware": "https://docs.langchain.com/langsmith/custom-middleware",
"/how-tos/http/custom_routes": "https://docs.langchain.com/langsmith/custom-routes",
"/cloud/concepts/data_storage_and_privacy": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
"/cloud/deployment/semantic_search": "https://docs.langchain.com/langsmith/semantic-search",
"/how-tos/ttl/configure_ttl": "https://docs.langchain.com/langsmith/configure-ttl",
"/concepts/deployment_options": "https://docs.langchain.com/langsmith/deployments",
"/cloud/quick_start": "https://docs.langchain.com/langsmith/deployment-quickstart",
"/cloud/deployment/setup": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
"/cloud/deployment/setup_pyproject": "https://docs.langchain.com/langsmith/setup-pyproject",
"/cloud/deployment/setup_javascript": "https://docs.langchain.com/langsmith/setup-javascript",
"/cloud/deployment/custom_docker": "https://docs.langchain.com/langsmith/custom-docker",
"/cloud/deployment/graph_rebuild": "https://docs.langchain.com/langsmith/graph-rebuild",
"/concepts/langgraph_cloud": "https://docs.langchain.com/langsmith/cloud",
"/concepts/langgraph_self_hosted_data_plane": "https://docs.langchain.com/langsmith/platform-setup",
"/concepts/langgraph_self_hosted_control_plane": "https://docs.langchain.com/langsmith/platform-setup",
"/concepts/langgraph_standalone_container": "https://docs.langchain.com/langsmith/docker",
"/cloud/deployment/cloud": "https://docs.langchain.com/langsmith/cloud",
"/cloud/deployment/self_hosted_data_plane": "https://docs.langchain.com/langsmith/platform-setup",
"/cloud/deployment/self_hosted_control_plane": "https://docs.langchain.com/langsmith/platform-setup",
"/cloud/deployment/standalone_container": "https://docs.langchain.com/langsmith/docker",
"/concepts/server-mcp": "https://docs.langchain.com/langsmith/server-mcp",
"/cloud/how-tos/human_in_the_loop_time_travel": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
"/cloud/how-tos/add-human-in-the-loop": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
"/cloud/deployment/egress": "https://docs.langchain.com/langsmith/env-var",
"/cloud/how-tos/streaming": "https://docs.langchain.com/langsmith/streaming",
"/cloud/reference/api/api_ref": "https://docs.langchain.com/langsmith/server-api-ref",
"/cloud/reference/langgraph_server_changelog": "https://docs.langchain.com/langsmith/agent-server-changelog",
"/cloud/reference/api/api_ref_control_plane": "https://docs.langchain.com/langsmith/api-ref-control-plane",
"/cloud/reference/cli": "https://docs.langchain.com/langsmith/cli",
"/cloud/reference/env_var": "https://docs.langchain.com/langsmith/env-var",
"/troubleshooting/studio": "https://docs.langchain.com/langsmith/troubleshooting-studio",
"/index": "https://docs.langchain.com/oss/python/langgraph/overview",
"/agents/agents": "https://docs.langchain.com/oss/python/langchain/agents",
"/concepts/why-langgraph": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/get-started/1-build-basic-chatbot": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/get-started/2-add-tools": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/get-started/3-add-memory": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/get-started/4-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/get-started/5-customize-state": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/get-started/6-time-travel": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/tutorials/langsmith/local-server": "https://docs.langchain.com/oss/python/langgraph/local-server",
"/tutorials/workflows": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/tutorials/plan-and-execute/plan-and-execute": "https://docs.langchain.com/oss/python/langchain/middleware/built-in#to-do-list",
"/tutorials/langgraph-platform/local-server/local-server": "https://docs.langchain.com/langsmith/local-server",
"/concepts/agentic_concepts": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/agents/overview": "https://docs.langchain.com/oss/python/langchain/agents",
"/agents/run_agents": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"/concepts/low_level": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/graph-api": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/react-agent-from-scratch": "https://docs.langchain.com/oss/python/langchain/quickstart",
"/concepts/functional_api": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"/how-tos/use-functional-api": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"/concepts/pregel": "https://docs.langchain.com/oss/python/langgraph/pregel",
"/concepts/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/concepts/persistence": "https://docs.langchain.com/oss/python/langgraph/persistence",
"/concepts/durable_execution": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
"/concepts/memory": "https://docs.langchain.com/oss/python/langgraph/memory",
"/how-tos/memory/add-memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/agents/context": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/agents/models": "https://docs.langchain.com/oss/python/langgraph/overview",
"/concepts/tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/how-tos/tool-calling": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/concepts/human_in_the_loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/how-tos/human_in_the_loop/add-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"/concepts/time-travel": "https://docs.langchain.com/oss/python/langgraph/persistence",
"/how-tos/human_in_the_loop/time-travel": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
"/concepts/subgraphs": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
"/how-tos/subgraph": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
"/concepts/multi_agent": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/agents/multi-agent": "https://docs.langchain.com/oss/python/langchain/multi-agent",
"/how-tos/multi_agent": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/concepts/mcp": "https://docs.langchain.com/oss/python/langgraph/overview",
"/agents/mcp": "https://docs.langchain.com/oss/python/langgraph/overview",
"/concepts/tracing": "https://docs.langchain.com/oss/python/langgraph/observability",
"/how-tos/enable-tracing": "https://docs.langchain.com/oss/python/langgraph/observability",
"/agents/evals": "https://docs.langchain.com/oss/python/langgraph/overview",
"/concepts/template_applications": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/rag/langgraph_agentic_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/multi_agent/agent_supervisor": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/tutorials/sql/sql-agent": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
"/agents/ui": "https://docs.langchain.com/oss/python/langgraph/ui",
"/how-tos/run-id-langsmith": "https://docs.langchain.com/oss/python/langgraph/observability",
"/troubleshooting/errors/index": "https://docs.langchain.com/oss/python/langgraph/common-errors",
"/troubleshooting/errors/INVALID_CHAT_HISTORY": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
"/troubleshooting/errors/INVALID_LICENSE": "https://docs.langchain.com/oss/python/langgraph/common-errors",
"/adopters": "https://docs.langchain.com/oss/python/langgraph/case-studies",
"/concepts/faq": "https://docs.langchain.com/oss/python/langgraph/overview",
"/agents/prebuilt": "https://docs.langchain.com/oss/python/langchain/agents",
"/reference/index": "https://reference.langchain.com/python/langgraph/",
"/reference/graphs": "https://reference.langchain.com/python/langgraph/graphs/",
"/reference/func": "https://reference.langchain.com/python/langgraph/func/",
"/reference/pregel": "https://reference.langchain.com/python/langgraph/pregel/",
"/reference/checkpoints": "https://reference.langchain.com/python/langgraph/checkpoints/",
"/reference/store": "https://reference.langchain.com/python/langgraph/store/",
"/reference/cache": "https://reference.langchain.com/python/langgraph/cache/",
"/reference/types": "https://reference.langchain.com/python/langgraph/types/",
"/reference/runtime": "https://reference.langchain.com/python/langgraph/runtime/",
"/reference/config": "https://reference.langchain.com/python/langgraph/config/",
"/reference/errors": "https://reference.langchain.com/python/langgraph/errors/",
"/reference/constants": "https://reference.langchain.com/python/langgraph/constants/",
"/reference/channels": "https://reference.langchain.com/python/langgraph/channels/",
"/reference/agents": "https://reference.langchain.com/python/langgraph/agents/",
"/reference/supervisor": "https://reference.langchain.com/python/langgraph/supervisor/",
"/reference/swarm": "https://reference.langchain.com/python/langgraph/swarm/",
"/reference/mcp": "https://reference.langchain.com/python/langgraph/mcp/",
"/cloud/reference/sdk/python_sdk_ref": "https://reference.langchain.com/python/langsmith/deployment/sdk/",
"/reference/remote_graph": "https://reference.langchain.com/python/langsmith/deployment/remote_graph/",
"/additional-resources/index": "https://docs.langchain.com/oss/python/langchain/overview",
"/cloud/reference/sdk/js_ts_sdk_ref": "https://reference.langchain.com/javascript/modules/langsmith.html",
"/snippets/chat_model_tabs": "https://docs.langchain.com/oss/python/langchain/overview",
"/troubleshooting/errors/GRAPH_RECURSION_LIMIT": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
"/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
"/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
"/troubleshooting/errors/MULTIPLE_SUBGRAPHS": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
"/tutorials/rag/langgraph_self_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/additional-resources": "https://docs.langchain.com/oss/python/langgraph/overview",
"/examples": "https://docs.langchain.com/oss/python/langgraph/overview",
"/guides": "https://docs.langchain.com/oss/python/langgraph/overview",
"/how-tos/autogen-integration-functional": "https://docs.langchain.com/oss/python/langgraph/overview",
"/how-tos/cross-thread-persistence-functional": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
"/how-tos/disable-streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
"/how-tos/memory/semantic-search": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/multi-agent-multi-turn-convo-functional": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/multi-agent-network-functional": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"/how-tos/persistence-functional": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"/how-tos/react-agent-from-scratch-functional": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"/reference": "https://reference.langchain.com/python/langgraph/",
"/troubleshooting/errors": "https://docs.langchain.com/oss/python/langgraph/common-errors",
"/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/chatbots/information-gather-prompting": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/extraction/retries": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/langgraph-platform/local-server": "https://docs.langchain.com/langsmith/agent-server",
"/tutorials/lats/lats": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/llm-compiler/LLMCompiler": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/rag/langgraph_adaptive_rag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/rag/langgraph_crag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/rag/langgraph_crag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/rag/langgraph_self_rag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"/tutorials/reflection/reflection": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/reflexion/reflexion": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/rewoo/rewoo": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/self-discover/self-discover": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/tnt-llm/tnt-llm": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/tot/tot": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/usaco/usaco": "https://docs.langchain.com/oss/python/langgraph/overview",
"/tutorials/web-navigation/web_voyager": "https://docs.langchain.com/oss/python/langgraph/overview"
}
-3
View File
@@ -1,3 +0,0 @@
# LangGraph examples
This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview). Please refer to the LangChain docs for the most up-to-date examples and usage guidelines for LangGraph.
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "10251c1c",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "c5fc63df",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a4351a24",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "4cc9af1e",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,203 +0,0 @@
import functools
from typing import Annotated, Any, Callable, Dict, List, Optional, Union
from langchain_community.adapters.openai import convert_message_to_dict
from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import Runnable, RunnableLambda
from langchain_core.runnables import chain as as_runnable
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph, START
def langchain_to_openai_messages(messages: List[BaseMessage]):
"""
Convert a list of langchain base messages to a list of openai messages.
Parameters:
messages (List[BaseMessage]): A list of langchain base messages.
Returns:
List[dict]: A list of openai messages.
"""
return [
convert_message_to_dict(m) if isinstance(m, BaseMessage) else m
for m in messages
]
def create_simulated_user(
system_prompt: str, llm: Runnable | None = None
) -> Runnable[Dict, AIMessage]:
"""
Creates a simulated user for chatbot simulation.
Args:
system_prompt (str): The system prompt to be used by the simulated user.
llm (Runnable | None, optional): The language model to be used for the simulation.
Defaults to gpt-3.5-turbo.
Returns:
Runnable[Dict, AIMessage]: The simulated user for chatbot simulation.
"""
return ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder(variable_name="messages"),
]
) | (llm or ChatOpenAI(model="gpt-3.5-turbo")).with_config(
run_name="simulated_user"
)
Messages = Union[list[AnyMessage], AnyMessage]
def add_messages(left: Messages, right: Messages) -> Messages:
if not isinstance(left, list):
left = [left]
if not isinstance(right, list):
right = [right]
return left + right
class SimulationState(TypedDict):
"""
Represents the state of a simulation.
Attributes:
messages (List[AnyMessage]): A list of messages in the simulation.
inputs (Optional[dict[str, Any]]): Optional inputs for the simulation.
"""
messages: Annotated[List[AnyMessage], add_messages]
inputs: Optional[dict[str, Any]]
def create_chat_simulator(
assistant: (
Callable[[List[AnyMessage]], str | AIMessage]
| Runnable[List[AnyMessage], str | AIMessage]
),
simulated_user: Runnable[Dict, AIMessage],
*,
input_key: str,
max_turns: int = 6,
should_continue: Optional[Callable[[SimulationState], str]] = None,
):
"""Creates a chat simulator for evaluating a chatbot.
Args:
assistant: The chatbot assistant function or runnable object.
simulated_user: The simulated user object.
input_key: The key for the input to the chat simulation.
max_turns: The maximum number of turns in the chat simulation. Default is 6.
should_continue: Optional function to determine if the simulation should continue.
If not provided, a default function will be used.
Returns:
The compiled chat simulation graph.
"""
graph_builder = StateGraph(SimulationState)
graph_builder.add_node(
"user",
_create_simulated_user_node(simulated_user),
)
graph_builder.add_node(
"assistant", _fetch_messages | assistant | _coerce_to_message
)
graph_builder.add_edge("assistant", "user")
graph_builder.add_conditional_edges(
"user",
should_continue or functools.partial(_should_continue, max_turns=max_turns),
)
# If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead.
graph_builder.add_edge(START, "assistant" if input_key is not None else "user")
return (
RunnableLambda(_prepare_example).bind(input_key=input_key)
| graph_builder.compile()
)
## Private methods
def _prepare_example(inputs: dict[str, Any], input_key: Optional[str] = None):
if input_key is not None:
if input_key not in inputs:
raise ValueError(
f"Dataset's example input must contain the provided input key: '{input_key}'.\nFound: {list(inputs.keys())}"
)
messages = [HumanMessage(content=inputs[input_key])]
return {
"inputs": {k: v for k, v in inputs.items() if k != input_key},
"messages": messages,
}
return {"inputs": inputs, "messages": []}
def _invoke_simulated_user(state: SimulationState, simulated_user: Runnable):
"""Invoke the simulated user node."""
runnable = (
simulated_user
if isinstance(simulated_user, Runnable)
else RunnableLambda(simulated_user)
)
inputs = state.get("inputs", {})
inputs["messages"] = state["messages"]
return runnable.invoke(inputs)
def _swap_roles(state: SimulationState):
new_messages = []
for m in state["messages"]:
if isinstance(m, AIMessage):
new_messages.append(HumanMessage(content=m.content))
else:
new_messages.append(AIMessage(content=m.content))
return {
"inputs": state.get("inputs", {}),
"messages": new_messages,
}
@as_runnable
def _fetch_messages(state: SimulationState):
"""Invoke the simulated user node."""
return state["messages"]
def _convert_to_human_message(message: BaseMessage):
return {"messages": [HumanMessage(content=message.content)]}
def _create_simulated_user_node(simulated_user: Runnable):
"""Simulated user accepts a {"messages": [...]} argument and returns a single message."""
return (
_swap_roles
| RunnableLambda(_invoke_simulated_user).bind(simulated_user=simulated_user)
| _convert_to_human_message
)
def _coerce_to_message(assistant_output: str | BaseMessage):
if isinstance(assistant_output, str):
return {"messages": [AIMessage(content=assistant_output)]}
else:
return {"messages": [assistant_output]}
def _should_continue(state: SimulationState, max_turns: int = 6):
messages = state["messages"]
# TODO support other stop criteria
if len(messages) > max_turns:
return END
elif messages[-1].content.strip() == "FINISHED":
return END
else:
return "assistant"
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a9014f94",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "f47ce992",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1f2f13ca",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "5e4c9bfe",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a8232bc9",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/customer-support/customer-support.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "63da8671",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8dbdba5b",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/extraction/retries.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "1d444b7f",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3ecab357",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "3f2866bd",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "09038b53",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/lats/lats.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "b1669748",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "85205e97",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "2fdab366",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "5cc8a2ad",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "b9f3508a",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d2b507b9",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/multi_agent/multi-agent-collaboration.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "41a8f10a",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9138f92e",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "093678ba",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "294995c4",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/react-agent-from-scratch.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -1,40 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "40f0d107",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/react-agent-structured-output.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "658773a2",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/reflection/reflection.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "1cb60657",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "caf07859",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/reflexion/reflexion.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "cd1df0e0",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "961f43ec",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/rewoo/rewoo.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "7f00c427",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-40
View File
@@ -1,40 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "bbd6e9b8",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/run-id-langsmith.md)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "f6db1873",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/self-discover/self-discover.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "219a78f9",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-40
View File
@@ -1,40 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "f49876e1",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/subgraph.md)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
-40
View File
@@ -1,40 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7fd8bd65",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/tool-calling.md)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "83c2223f",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/sql/sql-agent.md)"
]
},
{
"cell_type": "markdown",
"id": "57f924b1",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "11140167",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "1a2ba3e6",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9dffdb54",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/usaco/usaco.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "579c9959",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-41
View File
@@ -1,41 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "007ea2e9",
"metadata": {},
"source": [
"[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/web-navigation/web_voyager.ipynb)"
]
},
{
"cell_type": "markdown",
"id": "f0d7b895",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-12
View File
@@ -1,12 +0,0 @@
.PHONY: format lint test
format:
uv run ruff format .
uv run ruff check --fix .
lint:
uv run ruff check .
uv run ty check
test:
uv run pytest $(TEST)
-111
View File
@@ -1,111 +0,0 @@
# langgraph-checkpoint-conformance
Conformance test suite for [LangGraph](https://github.com/langchain-ai/langgraph) checkpointer implementations.
Validates that a `BaseCheckpointSaver` subclass correctly implements the checkpoint storage contract — blob round-trips, metadata preservation, namespace isolation, incremental channel updates, and more.
## Installation
```bash
pip install langgraph-checkpoint-conformance
```
## Quick start
Register your checkpointer with `@checkpointer_test` and run `validate()`:
```python
import asyncio
from langgraph.checkpoint.conformance import checkpointer_test, validate
@checkpointer_test(name="MyCheckpointer")
async def my_checkpointer():
saver = MyCheckpointer(...)
yield saver
# cleanup runs after yield
async def main():
report = await validate(my_checkpointer)
report.print_report()
assert report.passed_all_base()
asyncio.run(main())
```
Or in a pytest test:
```python
import pytest
from langgraph.checkpoint.conformance import checkpointer_test, validate
@checkpointer_test(name="MyCheckpointer")
async def my_checkpointer():
yield MyCheckpointer(...)
@pytest.mark.asyncio
async def test_conformance():
report = await validate(my_checkpointer)
report.print_report()
assert report.passed_all_base()
```
## Capabilities
The suite tests **base** capabilities (required) and **extended** capabilities (optional, auto-detected):
| Capability | Required | Method |
|---|---|---|
| `put` | yes | `aput` |
| `put_writes` | yes | `aput_writes` |
| `get_tuple` | yes | `aget_tuple` |
| `list` | yes | `alist` |
| `delete_thread` | yes | `adelete_thread` |
| `delete_for_runs` | no | `adelete_for_runs` |
| `copy_thread` | no | `acopy_thread` |
| `prune` | no | `aprune` |
Extended capabilities are detected by checking whether the method is overridden from `BaseCheckpointSaver`. If not overridden, those tests are skipped.
## Options
### Progress output
```python
from langgraph.checkpoint.conformance.report import ProgressCallbacks
# Dot-style progress (. per pass, F per fail)
report = await validate(my_checkpointer, progress=ProgressCallbacks.default())
# Verbose (per-test names + stacktraces on failure)
report = await validate(my_checkpointer, progress=ProgressCallbacks.verbose())
```
### Skip capabilities
```python
@checkpointer_test(name="MyCheckpointer", skip_capabilities={"prune"})
async def my_checkpointer():
yield MyCheckpointer(...)
```
### Run specific capabilities
```python
report = await validate(my_checkpointer, capabilities={"put", "list"})
```
### Lifespan (one-time setup/teardown)
For expensive setup like database creation:
```python
async def db_lifespan():
await create_database()
yield
await drop_database()
@checkpointer_test(name="PostgresSaver", lifespan=db_lifespan)
async def pg_checkpointer():
async with PostgresSaver.from_conn_string(CONN_STRING) as saver:
yield saver
```
@@ -1,9 +0,0 @@
"""langgraph-checkpoint-conformance: conformance test suite for checkpointer implementations."""
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.conformance.validate import validate
__all__ = [
"checkpointer_test",
"validate",
]
@@ -1,93 +0,0 @@
"""Capability detection for checkpointer implementations."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING
from langgraph.checkpoint.base import BaseCheckpointSaver
if TYPE_CHECKING:
pass
class Capability(str, Enum):
"""Capabilities that a checkpointer may support."""
PUT = "put"
PUT_WRITES = "put_writes"
GET_TUPLE = "get_tuple"
LIST = "list"
DELETE_THREAD = "delete_thread"
DELETE_FOR_RUNS = "delete_for_runs"
COPY_THREAD = "copy_thread"
PRUNE = "prune"
# Capabilities that every checkpointer must support.
BASE_CAPABILITIES = frozenset(
{
Capability.PUT,
Capability.PUT_WRITES,
Capability.GET_TUPLE,
Capability.LIST,
Capability.DELETE_THREAD,
}
)
# Capabilities that are optional extensions.
EXTENDED_CAPABILITIES = frozenset(
{
Capability.DELETE_FOR_RUNS,
Capability.COPY_THREAD,
Capability.PRUNE,
}
)
ALL_CAPABILITIES = BASE_CAPABILITIES | EXTENDED_CAPABILITIES
# Maps capability to the async method name on BaseCheckpointSaver (or subclass).
_CAPABILITY_METHOD_MAP: dict[Capability, str] = {
Capability.PUT: "aput",
Capability.PUT_WRITES: "aput_writes",
Capability.GET_TUPLE: "aget_tuple",
Capability.LIST: "alist",
Capability.DELETE_THREAD: "adelete_thread",
Capability.DELETE_FOR_RUNS: "adelete_for_runs",
Capability.COPY_THREAD: "acopy_thread",
Capability.PRUNE: "aprune",
}
@dataclass(frozen=True)
class DetectedCapabilities:
"""Result of capability detection for a checkpointer type."""
detected: frozenset[Capability]
missing: frozenset[Capability]
@classmethod
def from_instance(cls, saver: BaseCheckpointSaver) -> DetectedCapabilities:
"""Detect capabilities from a checkpointer instance."""
inner_type = type(saver)
detected: set[Capability] = set()
for cap, method_name in _CAPABILITY_METHOD_MAP.items():
if _is_overridden(inner_type, method_name):
detected.add(cap)
detected_fs = frozenset(detected)
return cls(
detected=detected_fs,
missing=ALL_CAPABILITIES - detected_fs,
)
def _is_overridden(inner_type: type, method: str) -> bool:
"""Check if *method* on *inner_type* differs from the base class default."""
base = getattr(BaseCheckpointSaver, method, None)
impl = getattr(inner_type, method, None)
if base is None or impl is None:
return impl is not None
return impl is not base
@@ -1,100 +0,0 @@
"""Checkpointer test registration and factory management."""
from __future__ import annotations
from collections.abc import AsyncGenerator, Callable
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any
from langgraph.checkpoint.base import BaseCheckpointSaver
# Type for the lifespan async context manager factory.
LifespanFactory = Callable[[], AsyncGenerator[None, None]]
# Module-level registry of decorated checkpointer factories.
_REGISTRY: dict[str, RegisteredCheckpointer] = {}
async def _noop_lifespan() -> AsyncGenerator[None, None]:
yield
@dataclass
class RegisteredCheckpointer:
"""A registered checkpointer test factory."""
name: str
factory: Callable[[], AsyncGenerator[BaseCheckpointSaver, None]]
skip_capabilities: set[str] = field(default_factory=set)
lifespan: LifespanFactory = _noop_lifespan
@asynccontextmanager
async def create(self) -> AsyncGenerator[BaseCheckpointSaver, None]:
"""Create a fresh checkpointer instance via the async generator."""
gen = self.factory()
try:
saver = await gen.__anext__()
yield saver
finally:
try:
await gen.__anext__()
except StopAsyncIteration:
pass
@asynccontextmanager
async def enter_lifespan(self) -> AsyncGenerator[None, None]:
"""Enter the lifespan context (once per validation run)."""
gen = self.lifespan()
try:
await gen.__anext__()
yield
finally:
try:
await gen.__anext__()
except StopAsyncIteration:
pass
def checkpointer_test(
name: str,
*,
skip_capabilities: set[str] | None = None,
lifespan: LifespanFactory | None = None,
) -> Callable[[Any], RegisteredCheckpointer]:
"""Register an async generator as a checkpointer test factory.
The factory is called once per capability suite to create a fresh
checkpointer. The optional `lifespan` is an async generator that
runs once for the entire validation run (e.g. to create/destroy a
database).
Example::
@checkpointer_test(name="InMemorySaver")
async def memory_checkpointer():
yield InMemorySaver()
With lifespan::
async def pg_lifespan():
await create_database()
yield
await drop_database()
@checkpointer_test(name="PostgresSaver", lifespan=pg_lifespan)
async def pg_checkpointer():
yield PostgresSaver(conn_string="...")
"""
def decorator(fn: Any) -> RegisteredCheckpointer:
registered = RegisteredCheckpointer(
name=name,
factory=fn,
skip_capabilities=skip_capabilities or set(),
lifespan=lifespan or _noop_lifespan,
)
_REGISTRY[name] = registered
return registered
return decorator
@@ -1,198 +0,0 @@
"""Capability report: results, progress callbacks, and pretty-printing."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from langgraph.checkpoint.conformance.capabilities import (
BASE_CAPABILITIES,
EXTENDED_CAPABILITIES,
Capability,
)
# Callback type for per-test progress reporting.
# (capability_name, test_name, passed, error_msg_or_None) -> None
OnTestResult = Callable[[str, str, bool, str | None], None]
# Callback type for capability-level events.
# (capability_name, detected) -> None
OnCapabilityStart = Callable[[str, bool], None]
class ProgressCallbacks:
"""Grouped callbacks for progress reporting during validation."""
def __init__(
self,
*,
on_capability_start: Callable[[str, bool], None] | None = None,
on_test_result: OnTestResult | None = None,
on_capability_end: Callable[[str], None] | None = None,
) -> None:
self.on_capability_start = on_capability_start
self.on_test_result = on_test_result
self.on_capability_end = on_capability_end
@classmethod
def default(cls) -> ProgressCallbacks:
"""Dot-style progress: ``.`` per pass, ``F`` per fail."""
def _cap_start(capability: str, detected: bool) -> None:
if detected:
print(f" {capability}: ", end="", flush=True)
else:
print(f"{capability} (not implemented)")
def _test_result(
capability: str, test_name: str, passed: bool, error: str | None
) -> None:
print("." if passed else "F", end="", flush=True)
def _cap_end(capability: str) -> None:
print() # newline after dots
return cls(
on_capability_start=_cap_start,
on_test_result=_test_result,
on_capability_end=_cap_end,
)
@classmethod
def verbose(cls) -> ProgressCallbacks:
"""Per-test output with names and errors."""
def _cap_start(capability: str, detected: bool) -> None:
if detected:
print(f" {capability}:")
else:
print(f"{capability} (not implemented)")
def _test_result(
capability: str, test_name: str, passed: bool, error: str | None
) -> None:
icon = "" if passed else ""
print(f" {icon} {test_name}")
if error:
for line in error.rstrip().splitlines():
print(f" {line}")
return cls(
on_capability_start=_cap_start,
on_test_result=_test_result,
)
@classmethod
def quiet(cls) -> ProgressCallbacks:
"""No progress output."""
return cls()
@dataclass
class CapabilityResult:
"""Result of running a single capability's test suite."""
detected: bool = False
passed: bool | None = None # None = skipped
tests_passed: int = 0
tests_failed: int = 0
tests_skipped: int = 0
failures: list[str] = field(default_factory=list)
@dataclass
class CapabilityReport:
"""Aggregate report across all capabilities."""
checkpointer_name: str
results: dict[str, CapabilityResult] = field(default_factory=dict)
def passed_all_base(self) -> bool:
"""Whether all base capability tests passed."""
for cap in BASE_CAPABILITIES:
result = self.results.get(cap.value)
if result is None or result.passed is not True:
return False
return True
def passed_all(self) -> bool:
"""Whether every detected capability's tests passed."""
for result in self.results.values():
if result.detected and result.passed is not True:
return False
return True
def conformance_level(self) -> str:
"""Return a human-readable conformance level string."""
if self.passed_all():
return "FULL"
if self.passed_all_base():
return "BASE+PARTIAL"
return "BASE" if self._any_base_passed() else "NONE"
def _any_base_passed(self) -> bool:
for cap in BASE_CAPABILITIES:
result = self.results.get(cap.value)
if result and result.passed is True:
return True
return False
def print_report(self) -> None:
"""Pretty-print the report to stdout."""
width = 52
border = "=" * width
print(f"\n{'':>2}{border}")
print(f"{'':>2} Checkpointer Validation: {self.checkpointer_name}")
print(f"{'':>2}{border}")
def _section(title: str, caps: frozenset[Capability]) -> None:
print(f"{'':>2} {title}")
for cap in sorted(caps, key=lambda c: c.value):
result = self.results.get(cap.value)
if result is None:
icon = " "
suffix = "(no tests)"
elif not result.detected:
icon = ""
suffix = "(not implemented)"
elif result.passed is True:
icon = ""
suffix = ""
elif result.passed is False:
icon = ""
suffix = f"({result.tests_failed} failed)"
else:
icon = ""
suffix = "(skipped)"
print(f"{'':>2} {icon} {cap.value:20s} {suffix}")
print()
_section("BASE CAPABILITIES", BASE_CAPABILITIES)
_section("EXTENDED CAPABILITIES", EXTENDED_CAPABILITIES)
total = sum(1 for r in self.results.values() if r.detected)
passed = sum(
1 for r in self.results.values() if r.detected and r.passed is True
)
level = self.conformance_level()
print(f"{'':>2} Result: {level} ({passed}/{total})")
print(f"{'':>2}{border}\n")
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-serializable dict."""
return {
"checkpointer_name": self.checkpointer_name,
"conformance_level": self.conformance_level(),
"results": {
name: {
"detected": r.detected,
"passed": r.passed,
"tests_passed": r.tests_passed,
"tests_failed": r.tests_failed,
"tests_skipped": r.tests_skipped,
"failures": r.failures,
}
for name, r in self.results.items()
},
}
@@ -1,27 +0,0 @@
"""Test spec modules for each checkpointer capability."""
from langgraph.checkpoint.conformance.spec.test_copy_thread import (
run_copy_thread_tests,
)
from langgraph.checkpoint.conformance.spec.test_delete_for_runs import (
run_delete_for_runs_tests,
)
from langgraph.checkpoint.conformance.spec.test_delete_thread import (
run_delete_thread_tests,
)
from langgraph.checkpoint.conformance.spec.test_get_tuple import run_get_tuple_tests
from langgraph.checkpoint.conformance.spec.test_list import run_list_tests
from langgraph.checkpoint.conformance.spec.test_prune import run_prune_tests
from langgraph.checkpoint.conformance.spec.test_put import run_put_tests
from langgraph.checkpoint.conformance.spec.test_put_writes import run_put_writes_tests
__all__ = [
"run_put_tests",
"run_put_writes_tests",
"run_get_tuple_tests",
"run_list_tests",
"run_delete_thread_tests",
"run_delete_for_runs_tests",
"run_copy_thread_tests",
"run_prune_tests",
]
@@ -1,250 +0,0 @@
"""COPY_THREAD capability tests — acopy_thread."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_source_thread(
saver: BaseCheckpointSaver,
tid: str,
*,
n: int = 3,
namespaces: list[str] | None = None,
) -> list[dict]:
"""Create n checkpoints on tid (optionally across namespaces). Returns stored configs."""
nss = namespaces or [""]
stored = []
for ns in nss:
parent_cfg = None
for i in range(n):
config = generate_config(tid, checkpoint_ns=ns)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint(channel_values={"step": i})
cp["channel_versions"] = {"step": 1}
parent_cfg = await saver.aput(
config, cp, generate_metadata(step=i), {"step": 1}
)
stored.append(parent_cfg)
return stored
async def test_copy_thread_basic(saver: BaseCheckpointSaver) -> None:
"""Checkpoints appear on target thread."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src)
await saver.acopy_thread(src, dst)
results = []
async for tup in saver.alist(generate_config(dst)):
results.append(tup)
assert len(results) == 3, f"Expected 3 copied checkpoints, got {len(results)}"
async def test_copy_thread_all_checkpoints(saver: BaseCheckpointSaver) -> None:
"""All checkpoints copied, not just latest."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=3)
await saver.acopy_thread(src, dst)
src_results = []
async for tup in saver.alist(generate_config(src)):
src_results.append(tup)
dst_results = []
async for tup in saver.alist(generate_config(dst)):
dst_results.append(tup)
assert len(dst_results) == len(src_results)
# Verify content matches
for s, d in zip(
sorted(src_results, key=lambda t: t.checkpoint["id"]),
sorted(dst_results, key=lambda t: t.checkpoint["id"]),
strict=True,
):
assert s.checkpoint["channel_values"] == d.checkpoint["channel_values"], (
f"channel_values mismatch for checkpoint {s.checkpoint['id']}"
)
async def test_copy_thread_preserves_metadata(
saver: BaseCheckpointSaver,
) -> None:
"""Metadata intact on copied checkpoints."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=2)
await saver.acopy_thread(src, dst)
src_tuples = []
async for tup in saver.alist(generate_config(src)):
src_tuples.append(tup)
dst_tuples = []
async for tup in saver.alist(generate_config(dst)):
dst_tuples.append(tup)
for s, d in zip(
sorted(src_tuples, key=lambda t: t.metadata.get("step", 0)),
sorted(dst_tuples, key=lambda t: t.metadata.get("step", 0)),
strict=True,
):
for key in s.metadata:
assert s.metadata.get(key) == d.metadata.get(key), (
f"metadata[{key!r}] mismatch: {s.metadata.get(key)!r} != {d.metadata.get(key)!r}"
)
async def test_copy_thread_preserves_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Root + child namespaces copied."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=1, namespaces=["", "child:1"])
await saver.acopy_thread(src, dst)
for ns in ["", "child:1"]:
results = []
async for tup in saver.alist(generate_config(dst, checkpoint_ns=ns)):
results.append(tup)
assert len(results) == 1, (
f"Expected 1 checkpoint in namespace '{ns}', got {len(results)}"
)
async def test_copy_thread_preserves_writes(saver: BaseCheckpointSaver) -> None:
"""Pending writes copied."""
src = str(uuid4())
dst = str(uuid4())
configs = await _setup_source_thread(saver, src, n=1)
# Add a write to the source
await saver.aput_writes(configs[-1], [("ch", "write_val")], str(uuid4()))
await saver.acopy_thread(src, dst)
tup = await saver.aget_tuple(generate_config(dst))
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "write_val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_copy_thread_preserves_ordering(
saver: BaseCheckpointSaver,
) -> None:
"""Checkpoint order maintained."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=4)
await saver.acopy_thread(src, dst)
src_ids = []
async for tup in saver.alist(generate_config(src)):
src_ids.append(tup.checkpoint["id"])
dst_ids = []
async for tup in saver.alist(generate_config(dst)):
dst_ids.append(tup.checkpoint["id"])
# Order should match (both newest-first)
assert src_ids == dst_ids
async def test_copy_thread_source_unchanged(saver: BaseCheckpointSaver) -> None:
"""Source thread still intact after copy."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=2)
# Snapshot source before copy
src_before = []
async for tup in saver.alist(generate_config(src)):
src_before.append(tup.checkpoint["id"])
await saver.acopy_thread(src, dst)
# Source should be unchanged
src_after = []
async for tup in saver.alist(generate_config(src)):
src_after.append(tup.checkpoint["id"])
assert src_before == src_after
async def test_copy_thread_nonexistent_source(
saver: BaseCheckpointSaver,
) -> None:
"""Graceful handling of non-existent source thread."""
src = str(uuid4())
dst = str(uuid4())
# Should not raise (or raise a known error)
try:
await saver.acopy_thread(src, dst)
except Exception:
pass # Some implementations may raise; that's acceptable
# Destination should be empty
results = []
async for tup in saver.alist(generate_config(dst)):
results.append(tup)
assert len(results) == 0
ALL_COPY_THREAD_TESTS = [
test_copy_thread_basic,
test_copy_thread_all_checkpoints,
test_copy_thread_preserves_metadata,
test_copy_thread_preserves_namespaces,
test_copy_thread_preserves_writes,
test_copy_thread_preserves_ordering,
test_copy_thread_source_unchanged,
test_copy_thread_nonexistent_source,
]
async def run_copy_thread_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all copy_thread tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_COPY_THREAD_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("copy_thread", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"copy_thread", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,218 +0,0 @@
"""DELETE_FOR_RUNS capability tests — adelete_for_runs."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _put_with_run_id(
saver: BaseCheckpointSaver,
tid: str,
run_id: str,
*,
checkpoint_ns: str = "",
parent_config: dict | None = None,
) -> dict:
"""Put a checkpoint with a run_id in metadata, return stored config."""
config = generate_config(tid, checkpoint_ns=checkpoint_ns)
if parent_config:
config["configurable"]["checkpoint_id"] = parent_config["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
md = generate_metadata(run_id=run_id)
return await saver.aput(config, cp, md, {})
async def test_delete_for_runs_single(saver: BaseCheckpointSaver) -> None:
"""One run_id removed."""
tid = str(uuid4())
run1, run2 = str(uuid4()), str(uuid4())
stored1 = await _put_with_run_id(saver, tid, run1)
await _put_with_run_id(saver, tid, run2, parent_config=stored1)
# Pre-delete: verify both runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, "Pre-delete: run1 should exist"
assert run2 in pre_run_ids, "Pre-delete: run2 should exist"
await saver.adelete_for_runs([run1])
# run1's checkpoint should be gone; run2 should remain
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
assert run2 in run_ids
async def test_delete_for_runs_multiple(saver: BaseCheckpointSaver) -> None:
"""List of run_ids removed."""
tid = str(uuid4())
run1, run2, run3 = str(uuid4()), str(uuid4()), str(uuid4())
s1 = await _put_with_run_id(saver, tid, run1)
s2 = await _put_with_run_id(saver, tid, run2, parent_config=s1)
await _put_with_run_id(saver, tid, run3, parent_config=s2)
# Pre-delete: verify all 3 runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, "Pre-delete: run1 should exist"
assert run2 in pre_run_ids, "Pre-delete: run2 should exist"
assert run3 in pre_run_ids, "Pre-delete: run3 should exist"
await saver.adelete_for_runs([run1, run2])
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
assert run2 not in run_ids
assert run3 in run_ids
async def test_delete_for_runs_preserves_other_runs(
saver: BaseCheckpointSaver,
) -> None:
"""Unrelated runs untouched."""
tid = str(uuid4())
run_keep = str(uuid4())
run_delete = str(uuid4())
await _put_with_run_id(saver, tid, run_keep)
await _put_with_run_id(saver, tid, run_delete)
# Pre-delete: verify both runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run_keep in pre_run_ids, "Pre-delete: run_keep should exist"
assert run_delete in pre_run_ids, "Pre-delete: run_delete should exist"
await saver.adelete_for_runs([run_delete])
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run_keep in run_ids
async def test_delete_for_runs_removes_writes(
saver: BaseCheckpointSaver,
) -> None:
"""Associated writes cleaned up."""
tid = str(uuid4())
run1 = str(uuid4())
stored = await _put_with_run_id(saver, tid, run1)
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
# Pre-delete: verify writes exist
pre_tup = await saver.aget_tuple(stored)
assert pre_tup is not None, "Pre-delete: checkpoint should exist"
assert pre_tup.pending_writes is not None and len(pre_tup.pending_writes) == 1, (
f"Pre-delete: expected 1 write, got {len(pre_tup.pending_writes) if pre_tup.pending_writes else 0}"
)
await saver.adelete_for_runs([run1])
# The checkpoint (and its writes) should be gone
tup = await saver.aget_tuple(stored)
assert tup is None
async def test_delete_for_runs_empty_list_noop(
saver: BaseCheckpointSaver,
) -> None:
"""Empty list no error."""
await saver.adelete_for_runs([])
async def test_delete_for_runs_nonexistent_noop(
saver: BaseCheckpointSaver,
) -> None:
"""Missing run_ids no error."""
await saver.adelete_for_runs([str(uuid4())])
async def test_delete_for_runs_across_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""All namespaces cleaned."""
tid = str(uuid4())
run1 = str(uuid4())
await _put_with_run_id(saver, tid, run1, checkpoint_ns="")
await _put_with_run_id(saver, tid, run1, checkpoint_ns="child:1")
# Pre-delete: verify run1 present in both namespaces
for ns in ["", "child:1"]:
pre_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, f"Pre-delete: run1 should exist in ns='{ns}'"
await saver.adelete_for_runs([run1])
for ns in ["", "child:1"]:
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
ALL_DELETE_FOR_RUNS_TESTS = [
test_delete_for_runs_single,
test_delete_for_runs_multiple,
test_delete_for_runs_preserves_other_runs,
test_delete_for_runs_removes_writes,
test_delete_for_runs_empty_list_noop,
test_delete_for_runs_nonexistent_noop,
test_delete_for_runs_across_namespaces,
]
async def run_delete_for_runs_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all delete_for_runs tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_DELETE_FOR_RUNS_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("delete_for_runs", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"delete_for_runs", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,149 +0,0 @@
"""DELETE_THREAD capability tests — adelete_thread."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_delete_thread_removes_checkpoints(
saver: BaseCheckpointSaver,
) -> None:
"""All checkpoints gone after delete."""
tid = str(uuid4())
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
# Pre-delete: verify data exists
assert await saver.aget_tuple(generate_config(tid)) is not None, (
"Pre-delete: checkpoint should exist"
)
await saver.adelete_thread(tid)
tup = await saver.aget_tuple(generate_config(tid))
assert tup is None
results = []
async for t in saver.alist(generate_config(tid)):
results.append(t)
assert len(results) == 0
async def test_delete_thread_removes_writes(saver: BaseCheckpointSaver) -> None:
"""Pending writes gone after delete."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
# Pre-delete: verify writes exist
pre_tup = await saver.aget_tuple(generate_config(tid))
assert pre_tup is not None, "Pre-delete: checkpoint should exist"
assert pre_tup.pending_writes is not None and len(pre_tup.pending_writes) == 1, (
f"Pre-delete: expected 1 write, got {len(pre_tup.pending_writes) if pre_tup.pending_writes else 0}"
)
await saver.adelete_thread(tid)
tup = await saver.aget_tuple(generate_config(tid))
assert tup is None
async def test_delete_thread_removes_all_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Root + child namespaces both removed."""
tid = str(uuid4())
for ns in ["", "child:1"]:
cfg = generate_config(tid, checkpoint_ns=ns)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
# Pre-delete: verify each namespace has data
for ns in ["", "child:1"]:
pre = await saver.aget_tuple(generate_config(tid, checkpoint_ns=ns))
assert pre is not None, f"Pre-delete: namespace '{ns}' should have data"
await saver.adelete_thread(tid)
for ns in ["", "child:1"]:
tup = await saver.aget_tuple(generate_config(tid, checkpoint_ns=ns))
assert tup is None
async def test_delete_thread_preserves_other_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Other threads untouched."""
tid1, tid2 = str(uuid4()), str(uuid4())
for tid in (tid1, tid2):
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
await saver.adelete_thread(tid1)
assert await saver.aget_tuple(generate_config(tid1)) is None
assert await saver.aget_tuple(generate_config(tid2)) is not None
async def test_delete_thread_nonexistent_noop(
saver: BaseCheckpointSaver,
) -> None:
"""No error for missing thread."""
# Should not raise
await saver.adelete_thread(str(uuid4()))
ALL_DELETE_THREAD_TESTS = [
test_delete_thread_removes_checkpoints,
test_delete_thread_removes_writes,
test_delete_thread_removes_all_namespaces,
test_delete_thread_preserves_other_threads,
test_delete_thread_nonexistent_noop,
]
async def run_delete_thread_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all delete_thread tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_DELETE_THREAD_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("delete_thread", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"delete_thread", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,253 +0,0 @@
"""GET_TUPLE capability tests — aget_tuple retrieval."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_get_tuple_nonexistent_returns_none(
saver: BaseCheckpointSaver,
) -> None:
"""Missing thread returns None."""
config = generate_config(str(uuid4()))
tup = await saver.aget_tuple(config)
assert tup is None
async def test_get_tuple_latest_when_no_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Returns newest checkpoint when no checkpoint_id in config."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
ids.append(cp["id"])
# Get without checkpoint_id — should return the latest
tup = await saver.aget_tuple(generate_config(tid))
assert tup is not None
assert tup.checkpoint["id"] == ids[-1]
assert tup.metadata["step"] == 2, (
f"Expected latest step=2, got {tup.metadata['step']}"
)
async def test_get_tuple_specific_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Returns exact match when checkpoint_id specified."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
await saver.aput(config2, cp2, generate_metadata(step=1), {})
# Fetch the first one specifically
tup = await saver.aget_tuple(stored1)
assert tup is not None
assert tup.checkpoint["id"] == cp1["id"]
async def test_get_tuple_config_structure(saver: BaseCheckpointSaver) -> None:
"""tuple.config has thread_id, checkpoint_ns, checkpoint_id."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
tup = await saver.aget_tuple(stored)
assert tup is not None
conf = tup.config["configurable"]
assert conf["thread_id"] == tid
assert conf.get("checkpoint_ns", "") == "", (
f"Expected checkpoint_ns='', got {conf.get('checkpoint_ns')!r}"
)
assert conf["checkpoint_id"] == cp["id"]
async def test_get_tuple_checkpoint_fields(saver: BaseCheckpointSaver) -> None:
"""All Checkpoint fields present."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint(channel_values={"k": "v"})
cp["channel_versions"] = {"k": 1}
stored = await saver.aput(config, cp, generate_metadata(), {"k": 1})
tup = await saver.aget_tuple(stored)
assert tup is not None
c = tup.checkpoint
assert c["id"] == cp["id"], f"id mismatch: {c['id']!r} != {cp['id']!r}"
assert c["v"] == 1, f"Expected v=1, got {c['v']!r}"
assert "ts" in c and c["ts"], "ts should be non-empty"
assert c["channel_values"] == {"k": "v"}, f"channel_values: {c['channel_values']!r}"
assert "channel_versions" in c
assert "versions_seen" in c
async def test_get_tuple_metadata(saver: BaseCheckpointSaver) -> None:
"""metadata populated correctly."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
md = generate_metadata(source="input", step=-1)
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata["source"] == "input"
assert tup.metadata["step"] == -1
async def test_get_tuple_parent_config(saver: BaseCheckpointSaver) -> None:
"""parent_config when parent exists, None otherwise."""
tid = str(uuid4())
# First checkpoint — no parent
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
tup1 = await saver.aget_tuple(stored1)
assert tup1 is not None
assert tup1.parent_config is None
# Second checkpoint — has parent
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.parent_config is not None
assert (
tup2.parent_config["configurable"]["checkpoint_id"]
== stored1["configurable"]["checkpoint_id"]
)
async def test_get_tuple_pending_writes(saver: BaseCheckpointSaver) -> None:
"""pending_writes from put_writes visible."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][0] == task_id, (
f"task_id mismatch: {tup.pending_writes[0][0]!r}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_get_tuple_respects_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns filtering."""
tid = str(uuid4())
cfg_root = generate_config(tid, checkpoint_ns="")
cp_root = generate_checkpoint()
stored_root = await saver.aput(cfg_root, cp_root, generate_metadata(), {})
cfg_child = generate_config(tid, checkpoint_ns="child:1")
cp_child = generate_checkpoint()
stored_child = await saver.aput(cfg_child, cp_child, generate_metadata(), {})
tup_root = await saver.aget_tuple(stored_root)
assert tup_root is not None
assert tup_root.checkpoint["id"] == cp_root["id"]
tup_child = await saver.aget_tuple(stored_child)
assert tup_child is not None
assert tup_child.checkpoint["id"] == cp_child["id"]
async def test_get_tuple_nonexistent_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Specific but missing checkpoint_id returns None."""
tid = str(uuid4())
nonexistent_id = str(uuid4())
# Put one checkpoint so the thread exists
config = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(config, cp, generate_metadata(), {})
# Ask for a non-existent checkpoint_id
bad_cfg = generate_config(tid, checkpoint_id=nonexistent_id)
tup = await saver.aget_tuple(bad_cfg)
assert tup is None
ALL_GET_TUPLE_TESTS = [
test_get_tuple_nonexistent_returns_none,
test_get_tuple_latest_when_no_checkpoint_id,
test_get_tuple_specific_checkpoint_id,
test_get_tuple_config_structure,
test_get_tuple_checkpoint_fields,
test_get_tuple_metadata,
test_get_tuple_parent_config,
test_get_tuple_pending_writes,
test_get_tuple_respects_namespace,
test_get_tuple_nonexistent_checkpoint_id,
]
async def run_get_tuple_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all get_tuple tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_GET_TUPLE_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("get_tuple", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"get_tuple", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,414 +0,0 @@
"""LIST capability tests — alist with various filters."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_list_data(saver: BaseCheckpointSaver) -> dict:
"""Populate saver with test data for list tests. Returns lookup info."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(4):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
source = "input" if i % 2 == 0 else "loop"
md = generate_metadata(source=source, step=i)
parent_cfg = await saver.aput(config, cp, md, {})
ids.append(cp["id"])
return {
"thread_id": tid,
"checkpoint_ids": ids,
"latest_config": parent_cfg,
}
async def test_list_all(saver: BaseCheckpointSaver) -> None:
"""No filters returns all checkpoints for the thread."""
data = await _setup_list_data(saver)
tid = data["thread_id"]
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 4
async def test_list_by_thread(saver: BaseCheckpointSaver) -> None:
"""Filter by thread_id — other threads not returned."""
data = await _setup_list_data(saver)
# List for a non-existent thread
results = []
async for tup in saver.alist(generate_config(str(uuid4()))):
results.append(tup)
assert len(results) == 0
# List for actual thread
results = []
async for tup in saver.alist(generate_config(data["thread_id"])):
results.append(tup)
assert len(results) == 4
async def test_list_by_namespace(saver: BaseCheckpointSaver) -> None:
"""Filter by checkpoint_ns."""
tid = str(uuid4())
# Root namespace
cfg1 = generate_config(tid, checkpoint_ns="")
cp1 = generate_checkpoint()
await saver.aput(cfg1, cp1, generate_metadata(), {})
# Child namespace
cfg2 = generate_config(tid, checkpoint_ns="child:1")
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(), {})
root_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="")):
root_results.append(tup)
assert len(root_results) == 1
child_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="child:1")):
child_results.append(tup)
assert len(child_results) == 1
async def test_list_ordering(saver: BaseCheckpointSaver) -> None:
"""Newest first (descending checkpoint_id)."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
results = []
async for tup in saver.alist(generate_config(data["thread_id"])):
results.append(tup.checkpoint["id"])
# Should be in reverse order (newest first)
assert results == list(reversed(ids))
async def test_list_metadata_filter_single_key(
saver: BaseCheckpointSaver,
) -> None:
"""filter={'source': 'input'} returns only input checkpoints."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "input"},
):
results.append(tup)
assert len(results) == 2, (
f"Expected 2 'input' checkpoints (steps 0,2), got {len(results)}"
)
for tup in results:
assert tup.metadata["source"] == "input"
async def test_list_metadata_filter_step(saver: BaseCheckpointSaver) -> None:
"""filter={'step': 1} returns matching checkpoints."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"step": 1},
):
results.append(tup)
assert len(results) == 1
assert results[0].metadata["step"] == 1
async def test_list_before(saver: BaseCheckpointSaver) -> None:
"""Pagination cursor — only checkpoints before the given one."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
# Use the 3rd checkpoint as the 'before' cursor (index 2)
before_cfg = generate_config(data["thread_id"], checkpoint_id=ids[2])
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
before=before_cfg,
):
results.append(tup)
# Should only include checkpoints before ids[2]
result_ids = [t.checkpoint["id"] for t in results]
assert ids[2] not in result_ids
assert ids[3] not in result_ids
assert set(result_ids) == {ids[0], ids[1]}, (
f"Expected {{ids[0], ids[1]}}, got {set(result_ids)}"
)
async def test_list_limit(saver: BaseCheckpointSaver) -> None:
"""limit=1, limit=N."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(generate_config(data["thread_id"]), limit=1):
results.append(tup)
assert len(results) == 1
results = []
async for tup in saver.alist(generate_config(data["thread_id"]), limit=2):
results.append(tup)
assert len(results) == 2
async def test_list_limit_plus_before(saver: BaseCheckpointSaver) -> None:
"""Pagination with limit."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
before_cfg = generate_config(data["thread_id"], checkpoint_id=ids[3])
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
before=before_cfg,
limit=1,
):
results.append(tup)
assert len(results) == 1
assert results[0].checkpoint["id"] == ids[2]
async def test_list_combined_thread_and_filter(
saver: BaseCheckpointSaver,
) -> None:
"""thread_id + metadata filter combined."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "loop"},
):
results.append(tup)
assert len(results) == 2, (
f"Expected 2 'loop' checkpoints (steps 1,3), got {len(results)}"
)
for tup in results:
assert tup.metadata["source"] == "loop"
async def test_list_empty_result(saver: BaseCheckpointSaver) -> None:
"""No matches returns empty."""
results = []
async for tup in saver.alist(
generate_config(str(uuid4())),
filter={"source": "nonexistent"},
):
results.append(tup)
assert len(results) == 0
async def test_list_includes_pending_writes(saver: BaseCheckpointSaver) -> None:
"""pending_writes in listed tuples."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert results[0].pending_writes is not None
assert len(results[0].pending_writes) == 1, (
f"Expected 1 write, got {len(results[0].pending_writes)}"
)
assert results[0].pending_writes[0][1] == "ch", (
f"channel mismatch: {results[0].pending_writes[0][1]!r}"
)
assert results[0].pending_writes[0][2] == "val", (
f"value mismatch: {results[0].pending_writes[0][2]!r}"
)
async def test_list_multiple_namespaces(saver: BaseCheckpointSaver) -> None:
"""Root namespace checkpoint listed correctly."""
tid = str(uuid4())
for ns in ["", "child:1", "child:2"]:
cfg = generate_config(tid, checkpoint_ns=ns)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
# List with root namespace filter — should return exactly the root checkpoint
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="")):
results.append(tup)
assert len(results) == 1, f"Expected 1 root checkpoint, got {len(results)}"
async def test_list_metadata_filter_multiple_keys(
saver: BaseCheckpointSaver,
) -> None:
"""filter with multiple keys — all must match."""
tid = str(uuid4())
# Create checkpoints with different metadata combos
for source, step in [("input", 1), ("loop", 1), ("input", 2)]:
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(source=source, step=step), {})
results = []
async for tup in saver.alist(
generate_config(tid),
filter={"source": "input", "step": 2},
):
results.append(tup)
assert len(results) == 1, (
f"Expected 1 match for source=input+step=2, got {len(results)}"
)
assert results[0].metadata["source"] == "input"
assert results[0].metadata["step"] == 2
async def test_list_metadata_filter_no_match(
saver: BaseCheckpointSaver,
) -> None:
"""Multi-key filter that matches nothing returns empty."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "update", "step": 99},
):
results.append(tup)
assert len(results) == 0
async def test_list_metadata_custom_keys(
saver: BaseCheckpointSaver,
) -> None:
"""Custom (non-standard) metadata keys are filterable."""
tid = str(uuid4())
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(score=42, run_id="run-abc"), {})
cfg2 = generate_config(tid)
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(score=99, run_id="run-xyz"), {})
# Filter by custom key
results = []
async for tup in saver.alist(
generate_config(tid),
filter={"score": 42},
):
results.append(tup)
assert len(results) == 1
assert results[0].metadata["score"] == 42
assert results[0].metadata["run_id"] == "run-abc"
async def test_list_global_search(
saver: BaseCheckpointSaver,
) -> None:
"""alist(None, filter=...) searches across all threads."""
tid1, tid2 = str(uuid4()), str(uuid4())
# Use a unique marker so we don't collide with other tests' data
marker = str(uuid4())
cfg1 = generate_config(tid1)
cp1 = generate_checkpoint()
await saver.aput(cfg1, cp1, generate_metadata(source="input", marker=marker), {})
cfg2 = generate_config(tid2)
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(source="loop", marker=marker), {})
# Search across all threads with filter
results = []
async for tup in saver.alist(None, filter={"source": "input", "marker": marker}):
results.append(tup)
assert len(results) == 1
assert results[0].config["configurable"]["thread_id"] == tid1
# Search with marker only — should find both
results = []
async for tup in saver.alist(None, filter={"marker": marker}):
results.append(tup)
assert len(results) == 2
ALL_LIST_TESTS = [
test_list_all,
test_list_by_thread,
test_list_by_namespace,
test_list_ordering,
test_list_metadata_filter_single_key,
test_list_metadata_filter_step,
test_list_metadata_filter_multiple_keys,
test_list_metadata_filter_no_match,
test_list_metadata_custom_keys,
test_list_global_search,
test_list_before,
test_list_limit,
test_list_limit_plus_before,
test_list_combined_thread_and_filter,
test_list_empty_result,
test_list_includes_pending_writes,
test_list_multiple_namespaces,
]
async def run_list_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all list tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_LIST_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("list", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("list", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,217 +0,0 @@
"""PRUNE capability tests — aprune(strategy)."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_thread(saver: BaseCheckpointSaver, tid: str, n: int = 3) -> list[dict]:
"""Create n checkpoints on tid. Returns stored configs."""
stored = []
parent_cfg = None
for i in range(n):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
stored.append(parent_cfg)
return stored
async def test_prune_keep_latest_single_thread(
saver: BaseCheckpointSaver,
) -> None:
"""Only latest checkpoint survives."""
tid = str(uuid4())
configs = await _setup_thread(saver, tid, n=4)
await saver.aprune([tid], strategy="keep_latest")
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== configs[-1]["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_multiple_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Each thread keeps its latest."""
tid1, tid2 = str(uuid4()), str(uuid4())
c1 = await _setup_thread(saver, tid1, n=3)
c2 = await _setup_thread(saver, tid2, n=2)
await saver.aprune([tid1, tid2], strategy="keep_latest")
for tid, expected_last in [(tid1, c1[-1]), (tid2, c2[-1])]:
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== expected_last["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_across_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Latest per namespace kept."""
tid = str(uuid4())
# Root namespace: 3 checkpoints
parent = None
for i in range(3):
cfg = generate_config(tid, checkpoint_ns="")
if parent:
cfg["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent = await saver.aput(cfg, cp, generate_metadata(step=i), {})
root_latest = parent
# Child namespace: 2 checkpoints
parent = None
for i in range(2):
cfg = generate_config(tid, checkpoint_ns="child:1")
if parent:
cfg["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent = await saver.aput(cfg, cp, generate_metadata(step=i), {})
child_latest = parent
assert root_latest is not None
assert child_latest is not None
await saver.aprune([tid], strategy="keep_latest")
for ns, expected in [("", root_latest), ("child:1", child_latest)]:
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== expected["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_preserves_writes(
saver: BaseCheckpointSaver,
) -> None:
"""Latest checkpoint's writes kept."""
tid = str(uuid4())
configs = await _setup_thread(saver, tid, n=3)
# Add writes to the latest
await saver.aput_writes(configs[-1], [("ch", "val")], str(uuid4()))
await saver.aprune([tid], strategy="keep_latest")
tup = await saver.aget_tuple(generate_config(tid))
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_prune_delete_all(saver: BaseCheckpointSaver) -> None:
"""delete_all strategy removes everything."""
tid = str(uuid4())
await _setup_thread(saver, tid, n=3)
await saver.aprune([tid], strategy="delete")
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 0
async def test_prune_preserves_other_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Unlisted threads untouched."""
tid1, tid2 = str(uuid4()), str(uuid4())
await _setup_thread(saver, tid1, n=3)
await _setup_thread(saver, tid2, n=2)
# Snapshot tid2 before prune
pre_ids = []
async for tup in saver.alist(generate_config(tid2)):
pre_ids.append(tup.checkpoint["id"])
await saver.aprune([tid1], strategy="keep_latest")
# tid2 should be fully intact — same checkpoint IDs
post_ids = []
async for tup in saver.alist(generate_config(tid2)):
post_ids.append(tup.checkpoint["id"])
assert post_ids == pre_ids, f"tid2 changed: {pre_ids} -> {post_ids}"
async def test_prune_empty_list_noop(saver: BaseCheckpointSaver) -> None:
"""Empty thread_ids no error."""
await saver.aprune([], strategy="keep_latest")
async def test_prune_nonexistent_noop(saver: BaseCheckpointSaver) -> None:
"""Missing threads no error."""
await saver.aprune([str(uuid4())], strategy="keep_latest")
ALL_PRUNE_TESTS = [
test_prune_keep_latest_single_thread,
test_prune_keep_latest_multiple_threads,
test_prune_keep_latest_across_namespaces,
test_prune_keep_latest_preserves_writes,
test_prune_delete_all,
test_prune_preserves_other_threads,
test_prune_empty_list_noop,
test_prune_nonexistent_noop,
]
async def run_prune_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all prune tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PRUNE_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("prune", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("prune", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,411 +0,0 @@
"""PUT capability tests — aput + aget_tuple round-trip."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from typing import Any
from uuid import uuid4
from langgraph.checkpoint.base import (
BaseCheckpointSaver,
ChannelVersions,
)
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_put_returns_config(saver: BaseCheckpointSaver) -> None:
"""aput returns a RunnableConfig with thread_id, checkpoint_ns, checkpoint_id."""
config = generate_config()
cp = generate_checkpoint(channel_values={"k": "v"})
cp["channel_versions"] = {"k": 1}
md = generate_metadata()
result = await saver.aput(config, cp, md, {"k": 1})
assert "configurable" in result
conf = result["configurable"]
assert "thread_id" in conf
assert "checkpoint_ns" in conf
assert "checkpoint_id" in conf
assert conf["checkpoint_id"] == cp["id"]
async def test_put_roundtrip(saver: BaseCheckpointSaver) -> None:
"""put then get_tuple returns identical checkpoint."""
config = generate_config()
cp = generate_checkpoint(channel_values={"msg": "hello"})
cp["channel_versions"] = {"msg": 1}
md = generate_metadata(source="input", step=-1)
stored_config = await saver.aput(config, cp, md, {"msg": 1})
tup = await saver.aget_tuple(stored_config)
assert tup is not None
assert tup.checkpoint["id"] == cp["id"]
assert tup.checkpoint["channel_values"] == {"msg": "hello"}
async def test_put_preserves_channel_values(saver: BaseCheckpointSaver) -> None:
"""Various types (str, int, list, dict, bytes, None) round-trip correctly."""
values: dict[str, Any] = {
"str_val": "hello",
"int_val": 42,
"list_val": [1, 2, 3],
"dict_val": {"nested": True},
}
config = generate_config()
cp = generate_checkpoint(channel_values=values)
versions: ChannelVersions = {k: 1 for k in values}
cp["channel_versions"] = versions
md = generate_metadata()
stored = await saver.aput(config, cp, md, versions)
tup = await saver.aget_tuple(stored)
assert tup is not None
for k, v in values.items():
assert tup.checkpoint["channel_values"].get(k) == v, (
f"channel_values[{k}]: expected {v!r}, got {tup.checkpoint['channel_values'].get(k)!r}"
)
async def test_put_preserves_channel_versions(saver: BaseCheckpointSaver) -> None:
"""ChannelVersions round-trip correctly."""
versions: ChannelVersions = {"a": 1, "b": 2}
config = generate_config()
cp = generate_checkpoint(
channel_values={"a": "x", "b": "y"}, channel_versions=versions
)
md = generate_metadata()
stored = await saver.aput(config, cp, md, versions)
tup = await saver.aget_tuple(stored)
assert tup is not None
# Compare version values — checkpointers may convert int to str
for k, expected in versions.items():
actual = tup.checkpoint["channel_versions"].get(k)
assert actual is not None, f"channel_versions[{k}] missing"
assert str(actual).split(".")[0] == str(expected).split(".")[0], (
f"channel_versions[{k}]: expected {expected!r}, got {actual!r}"
)
async def test_put_preserves_versions_seen(saver: BaseCheckpointSaver) -> None:
"""versions_seen dict round-trips."""
vs: dict[str, ChannelVersions] = {"node1": {"ch": 1}, "node2": {"ch": 2}}
config = generate_config()
cp = generate_checkpoint(versions_seen=vs)
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
for node in vs:
assert node in tup.checkpoint["versions_seen"], f"versions_seen[{node}] missing"
async def test_put_preserves_metadata(saver: BaseCheckpointSaver) -> None:
"""Metadata source, step, parents, and custom keys round-trip."""
md = generate_metadata(source="loop", step=3, custom_key="custom_value")
config = generate_config()
cp = generate_checkpoint()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata["source"] == "loop"
assert tup.metadata["step"] == 3
assert tup.metadata.get("custom_key") == "custom_value"
async def test_put_root_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns='' works."""
config = generate_config(checkpoint_ns="")
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.config["configurable"].get("checkpoint_ns", "") == ""
async def test_put_child_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns='child:abc' works."""
config = generate_config(checkpoint_ns="child:abc")
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.config["configurable"]["checkpoint_ns"] == "child:abc"
async def test_put_default_namespace(saver: BaseCheckpointSaver) -> None:
"""Config without checkpoint_ns defaults to ''."""
tid = str(uuid4())
config = {"configurable": {"thread_id": tid, "checkpoint_ns": ""}}
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
async def test_put_multiple_checkpoints_same_thread(
saver: BaseCheckpointSaver,
) -> None:
"""Sequential puts on same thread, all retrievable."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg is not None:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
md = generate_metadata(step=i)
parent_cfg = await saver.aput(config, cp, md, {})
ids.append(cp["id"])
# All three should be retrievable
for cid in ids:
cfg = generate_config(tid, checkpoint_id=cid)
tup = await saver.aget_tuple(cfg)
assert tup is not None, f"checkpoint {cid} not found"
assert tup.checkpoint["id"] == cid
async def test_put_multiple_threads_isolated(saver: BaseCheckpointSaver) -> None:
"""Different thread_ids don't interfere."""
tid1, tid2 = str(uuid4()), str(uuid4())
config1 = generate_config(tid1)
cp1 = generate_checkpoint(channel_values={"x": "thread1"})
cp1["channel_versions"] = {"x": 1}
await saver.aput(config1, cp1, generate_metadata(), {"x": 1})
config2 = generate_config(tid2)
cp2 = generate_checkpoint(channel_values={"x": "thread2"})
cp2["channel_versions"] = {"x": 1}
await saver.aput(config2, cp2, generate_metadata(), {"x": 1})
tup1 = await saver.aget_tuple(generate_config(tid1))
tup2 = await saver.aget_tuple(generate_config(tid2))
assert tup1 is not None and tup2 is not None
assert tup1.checkpoint["channel_values"]["x"] == "thread1"
assert tup2.checkpoint["channel_values"]["x"] == "thread2"
async def test_put_parent_config(saver: BaseCheckpointSaver) -> None:
"""parent checkpoint_id tracked correctly."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
# Second checkpoint — its config carries the parent checkpoint_id
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup = await saver.aget_tuple(stored2)
assert tup is not None
assert tup.parent_config is not None
assert (
tup.parent_config["configurable"]["checkpoint_id"]
== stored1["configurable"]["checkpoint_id"]
)
async def test_put_incremental_channel_update(saver: BaseCheckpointSaver) -> None:
"""Only updated channels need new blobs; unchanged channels loaded from prior versions."""
tid = str(uuid4())
# Checkpoint 1: both channels are new
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1", "b": "v2"},
channel_versions={"a": 1, "b": 1},
)
stored1 = await saver.aput(
config1, cp1, generate_metadata(step=0), {"a": 1, "b": 1}
)
# Checkpoint 2: only 'a' is updated
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1_updated", "b": "v2"},
channel_versions={"a": 2, "b": 1},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"a": 2})
# cp2 should reconstruct full channel_values from blobs at mixed versions
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1_updated", (
f"a: expected 'v1_updated', got {tup2.checkpoint['channel_values'].get('a')!r}"
)
assert tup2.checkpoint["channel_values"].get("b") == "v2", (
f"b: expected 'v2', got {tup2.checkpoint['channel_values'].get('b')!r}"
)
# cp1 should still return original values
tup1 = await saver.aget_tuple(stored1)
assert tup1 is not None
assert tup1.checkpoint["channel_values"].get("a") == "v1"
assert tup1.checkpoint["channel_values"].get("b") == "v2"
async def test_put_new_channel_added(saver: BaseCheckpointSaver) -> None:
"""A channel that appears for the first time in a later checkpoint."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1"},
channel_versions={"a": 1},
)
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {"a": 1})
# Checkpoint 2: 'b' is brand new, 'a' is unchanged
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1", "b": "new_channel"},
channel_versions={"a": 1, "b": 1},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"b": 1})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1", (
f"a: expected 'v1', got {tup2.checkpoint['channel_values'].get('a')!r}"
)
assert tup2.checkpoint["channel_values"].get("b") == "new_channel", (
f"b: expected 'new_channel', got {tup2.checkpoint['channel_values'].get('b')!r}"
)
async def test_put_channel_removed(saver: BaseCheckpointSaver) -> None:
"""Channel no longer in channel_versions should not appear in loaded values."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1", "b": "v2"},
channel_versions={"a": 1, "b": 1},
)
stored1 = await saver.aput(
config1, cp1, generate_metadata(step=0), {"a": 1, "b": 1}
)
# Checkpoint 2: 'b' dropped from channel_versions
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1_updated"},
channel_versions={"a": 2},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"a": 2})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1_updated"
assert "b" not in tup2.checkpoint["channel_values"], (
f"'b' should not be present, got {tup2.checkpoint['channel_values']}"
)
async def test_put_preserves_run_id(saver: BaseCheckpointSaver) -> None:
"""run_id in metadata round-trips correctly."""
run_id = str(uuid4())
config = generate_config()
cp = generate_checkpoint()
md = generate_metadata(source="loop", step=0, run_id=run_id)
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata.get("run_id") == run_id, (
f"run_id: expected {run_id!r}, got {tup.metadata.get('run_id')!r}"
)
async def test_put_preserves_versions_seen_values(saver: BaseCheckpointSaver) -> None:
"""versions_seen values (not just keys) round-trip correctly."""
vs: dict[str, ChannelVersions] = {
"node1": {"ch_a": 1, "ch_b": 2},
"node2": {"ch_a": 3},
}
config = generate_config()
cp = generate_checkpoint(versions_seen=vs)
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
for node, expected_versions in vs.items():
assert node in tup.checkpoint["versions_seen"], f"versions_seen[{node}] missing"
actual_versions = tup.checkpoint["versions_seen"][node]
for ch, expected_v in expected_versions.items():
actual_v = actual_versions.get(ch)
assert actual_v is not None, f"versions_seen[{node}][{ch}] missing"
assert str(actual_v).split(".")[0] == str(expected_v).split(".")[0], (
f"versions_seen[{node}][{ch}]: expected {expected_v!r}, got {actual_v!r}"
)
ALL_PUT_TESTS = [
test_put_returns_config,
test_put_roundtrip,
test_put_preserves_channel_values,
test_put_preserves_channel_versions,
test_put_preserves_versions_seen,
test_put_preserves_metadata,
test_put_root_namespace,
test_put_child_namespace,
test_put_default_namespace,
test_put_multiple_checkpoints_same_thread,
test_put_multiple_threads_isolated,
test_put_parent_config,
test_put_incremental_channel_update,
test_put_new_channel_added,
test_put_channel_removed,
test_put_preserves_run_id,
test_put_preserves_versions_seen_values,
]
async def run_put_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all put tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PUT_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("put", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("put", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,302 +0,0 @@
"""PUT_WRITES capability tests — aput_writes + pending_writes retrieval."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_put_writes_basic(saver: BaseCheckpointSaver) -> None:
"""Write stored, visible in aget_tuple pending_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("channel1", "value1")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
# Verify exact write tuple: (task_id, channel, value)
matching = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "channel1"]
assert len(matching) == 1, f"Expected 1 write, got {len(matching)}"
assert matching[0][2] == "value1", f"Value mismatch: {matching[0][2]!r}"
async def test_put_writes_multiple_writes_same_task(
saver: BaseCheckpointSaver,
) -> None:
"""Multiple (channel, value) pairs in a single call."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
writes = [("ch1", "v1"), ("ch2", "v2"), ("ch3", "v3")]
await saver.aput_writes(stored, writes, task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 3, (
f"Expected 3 writes, got {len(tup.pending_writes)}"
)
channels = {w[1] for w in tup.pending_writes}
assert channels == {"ch1", "ch2", "ch3"}, f"Expected exact channels, got {channels}"
# Verify values per channel
for expected_ch, expected_val in writes:
match = [
w for w in tup.pending_writes if w[0] == task_id and w[1] == expected_ch
]
assert len(match) == 1, f"Expected 1 write for {expected_ch}, got {len(match)}"
assert match[0][2] == expected_val, (
f"Value mismatch for {expected_ch}: {match[0][2]!r}"
)
async def test_put_writes_multiple_tasks(saver: BaseCheckpointSaver) -> None:
"""Different task_ids produce separate writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
t1, t2 = str(uuid4()), str(uuid4())
await saver.aput_writes(stored, [("ch", "from_t1")], t1)
await saver.aput_writes(stored, [("ch", "from_t2")], t2)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 2, (
f"Expected 2 writes, got {len(tup.pending_writes)}"
)
# Verify values per task
t1_writes = [w for w in tup.pending_writes if w[0] == t1 and w[1] == "ch"]
t2_writes = [w for w in tup.pending_writes if w[0] == t2 and w[1] == "ch"]
assert len(t1_writes) == 1, f"Expected 1 write from t1, got {len(t1_writes)}"
assert len(t2_writes) == 1, f"Expected 1 write from t2, got {len(t2_writes)}"
assert t1_writes[0][2] == "from_t1", f"t1 value: {t1_writes[0][2]!r}"
assert t2_writes[0][2] == "from_t2", f"t2 value: {t2_writes[0][2]!r}"
async def test_put_writes_preserves_task_id(saver: BaseCheckpointSaver) -> None:
"""task_id in pending_writes matches what was passed to aput_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert any(w[0] == task_id for w in tup.pending_writes)
async def test_put_writes_preserves_channel_and_value(
saver: BaseCheckpointSaver,
) -> None:
"""Channel name + value round-trip."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("my_channel", {"data": 123})], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
match = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "my_channel"]
assert len(match) == 1, f"Expected 1 write, got {len(match)}"
assert match[0][2] == {"data": 123}, f"Value mismatch: {match[0][2]!r}"
async def test_put_writes_task_path(saver: BaseCheckpointSaver) -> None:
"""task_path parameter accepted without error."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
# Should not raise
await saver.aput_writes(stored, [("ch", "v")], task_id, task_path="a:b:c")
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1
async def test_put_writes_idempotent(saver: BaseCheckpointSaver) -> None:
"""Duplicate (task_id, idx) doesn't duplicate writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected exactly 1 write total, got {len(tup.pending_writes)}"
)
# Should not have duplicated
matching = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "ch"]
assert len(matching) == 1
async def test_put_writes_special_channels(saver: BaseCheckpointSaver) -> None:
"""ERROR and INTERRUPT channels handled correctly."""
from langgraph.checkpoint.serde.types import ERROR, INTERRUPT
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(
stored,
[(ERROR, "something went wrong"), (INTERRUPT, {"reason": "human_input"})],
task_id,
)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
channels = {w[1] for w in tup.pending_writes}
assert ERROR in channels
assert INTERRUPT in channels
# Verify values
err_writes = [w for w in tup.pending_writes if w[0] == task_id and w[1] == ERROR]
assert len(err_writes) == 1, f"Expected 1 ERROR write, got {len(err_writes)}"
assert err_writes[0][2] == "something went wrong", (
f"ERROR value: {err_writes[0][2]!r}"
)
int_writes = [
w for w in tup.pending_writes if w[0] == task_id and w[1] == INTERRUPT
]
assert len(int_writes) == 1, f"Expected 1 INTERRUPT write, got {len(int_writes)}"
assert int_writes[0][2] == {"reason": "human_input"}, (
f"INTERRUPT value: {int_writes[0][2]!r}"
)
async def test_put_writes_across_namespaces(saver: BaseCheckpointSaver) -> None:
"""Writes isolated by checkpoint_ns."""
tid = str(uuid4())
# Root namespace checkpoint + write
cfg_root = generate_config(tid, checkpoint_ns="")
cp_root = generate_checkpoint()
stored_root = await saver.aput(cfg_root, cp_root, generate_metadata(), {})
root_task = str(uuid4())
await saver.aput_writes(stored_root, [("ch", "root_val")], root_task)
# Child namespace checkpoint + write
cfg_child = generate_config(tid, checkpoint_ns="child:1")
cp_child = generate_checkpoint()
stored_child = await saver.aput(cfg_child, cp_child, generate_metadata(), {})
child_task = str(uuid4())
await saver.aput_writes(stored_child, [("ch", "child_val")], child_task)
# Verify isolation — root should have exactly 1 write with root_val
tup_root = await saver.aget_tuple(stored_root)
assert tup_root is not None
assert tup_root.pending_writes is not None
root_ch = [w for w in tup_root.pending_writes if w[1] == "ch"]
assert len(root_ch) == 1, f"Expected 1 root write, got {len(root_ch)}"
assert root_ch[0][2] == "root_val", f"Root value: {root_ch[0][2]!r}"
# Child should have exactly 1 write with child_val
tup_child = await saver.aget_tuple(stored_child)
assert tup_child is not None
assert tup_child.pending_writes is not None
child_ch = [w for w in tup_child.pending_writes if w[1] == "ch"]
assert len(child_ch) == 1, f"Expected 1 child write, got {len(child_ch)}"
assert child_ch[0][2] == "child_val", f"Child value: {child_ch[0][2]!r}"
async def test_put_writes_cleared_on_next_checkpoint(
saver: BaseCheckpointSaver,
) -> None:
"""New checkpoint starts with fresh pending_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config, cp1, generate_metadata(step=0), {})
await saver.aput_writes(stored1, [("ch", "old_write")], str(uuid4()))
# New checkpoint
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup = await saver.aget_tuple(stored2)
assert tup is not None
# New checkpoint should have no pending writes
writes = tup.pending_writes or []
assert len(writes) == 0
ALL_PUT_WRITES_TESTS = [
test_put_writes_basic,
test_put_writes_multiple_writes_same_task,
test_put_writes_multiple_tasks,
test_put_writes_preserves_task_id,
test_put_writes_preserves_channel_and_value,
test_put_writes_task_path,
test_put_writes_idempotent,
test_put_writes_special_channels,
test_put_writes_across_namespaces,
test_put_writes_cleared_on_next_checkpoint,
]
async def run_put_writes_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all put_writes tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PUT_WRITES_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("put_writes", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"put_writes", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,212 +0,0 @@
"""Test utilities: checkpoint generators, assertion helpers, bulk operations."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
)
from langgraph.checkpoint.base.id import uuid6
def generate_checkpoint(
*,
checkpoint_id: str | None = None,
channel_values: dict[str, Any] | None = None,
channel_versions: ChannelVersions | None = None,
versions_seen: dict[str, ChannelVersions] | None = None,
) -> Checkpoint:
"""Create a well-formed Checkpoint with sensible defaults."""
return Checkpoint(
v=1,
id=checkpoint_id or str(uuid6(clock_seq=-1)),
ts=datetime.now(timezone.utc).isoformat(),
channel_values=channel_values if channel_values is not None else {},
channel_versions=channel_versions if channel_versions is not None else {},
versions_seen=versions_seen if versions_seen is not None else {},
pending_sends=[], # ty: ignore[invalid-key]
updated_channels=None,
)
def generate_config(
thread_id: str | None = None,
*,
checkpoint_ns: str = "",
checkpoint_id: str | None = None,
) -> RunnableConfig:
"""Create a RunnableConfig targeting a specific thread / namespace / checkpoint."""
configurable: dict[str, Any] = {
"thread_id": thread_id or str(uuid4()),
"checkpoint_ns": checkpoint_ns,
}
if checkpoint_id is not None:
configurable["checkpoint_id"] = checkpoint_id
return {"configurable": configurable}
def generate_metadata(
source: str = "loop",
step: int = 0,
**extra: Any,
) -> CheckpointMetadata:
"""Create CheckpointMetadata with defaults."""
md: dict[str, Any] = {"source": source, "step": step, "parents": {}}
md.update(extra)
return md
async def put_test_checkpoint(
saver: Any,
*,
thread_id: str | None = None,
checkpoint_ns: str = "",
parent_config: RunnableConfig | None = None,
channel_values: dict[str, Any] | None = None,
channel_versions: ChannelVersions | None = None,
metadata: CheckpointMetadata | None = None,
new_versions: ChannelVersions | None = None,
) -> RunnableConfig:
"""Put a single test checkpoint and return the stored config.
Handles wiring up parent_config, channel_values -> new_versions, etc.
"""
tid = thread_id or str(uuid4())
cp = generate_checkpoint(
channel_values=channel_values,
channel_versions=channel_versions,
)
# When channel_values are provided, ensure channel_versions + new_versions
# are consistent so the checkpointer stores the blobs correctly.
vals = channel_values or {}
cv = channel_versions
if cv is None and vals:
cv = {k: 1 for k in vals}
cp["channel_versions"] = cv
nv = new_versions
if nv is None:
nv = cv or {}
md = metadata or generate_metadata()
config = generate_config(tid, checkpoint_ns=checkpoint_ns)
if parent_config is not None:
config["configurable"]["checkpoint_id"] = parent_config["configurable"][
"checkpoint_id"
]
return await saver.aput(config, cp, md, nv)
async def put_test_checkpoints(
saver: Any,
*,
n_threads: int = 1,
n_checkpoints: int = 1,
namespaces: list[str] | None = None,
channel_values: dict[str, Any] | None = None,
) -> list[RunnableConfig]:
"""Convenience: put multiple checkpoints across threads/namespaces.
Returns the stored configs in insertion order.
"""
nss = namespaces or [""]
stored: list[RunnableConfig] = []
for t in range(n_threads):
tid = f"thread-{t}"
for ns in nss:
parent: RunnableConfig | None = None
for _c in range(n_checkpoints):
cfg = await put_test_checkpoint(
saver,
thread_id=tid,
checkpoint_ns=ns,
parent_config=parent,
channel_values=channel_values,
)
parent = cfg
stored.append(cfg)
return stored
def assert_checkpoint_equal(
actual: Checkpoint,
expected: Checkpoint,
*,
check_channel_values: bool = True,
) -> None:
"""Assert two checkpoints are semantically equal."""
assert actual["v"] == expected["v"], f"v mismatch: {actual['v']} != {expected['v']}"
assert actual["id"] == expected["id"], (
f"id mismatch: {actual['id']} != {expected['id']}"
)
assert actual["channel_versions"] == expected["channel_versions"], (
"channel_versions mismatch"
)
assert actual["versions_seen"] == expected["versions_seen"], (
"versions_seen mismatch"
)
if check_channel_values:
assert actual["channel_values"] == expected["channel_values"], (
"channel_values mismatch"
)
def assert_tuple_equal(
actual: CheckpointTuple,
expected: CheckpointTuple,
*,
check_writes: bool = True,
check_channel_values: bool = True,
) -> None:
"""Assert two CheckpointTuples are semantically equal."""
# Config
a_conf = actual.config["configurable"]
e_conf = expected.config["configurable"]
assert a_conf["thread_id"] == e_conf["thread_id"], (
f"thread_id mismatch: {a_conf['thread_id']} != {e_conf['thread_id']}"
)
assert a_conf.get("checkpoint_ns", "") == e_conf.get("checkpoint_ns", ""), (
"checkpoint_ns mismatch"
)
assert a_conf["checkpoint_id"] == e_conf["checkpoint_id"], "checkpoint_id mismatch"
# Checkpoint
assert_checkpoint_equal(
actual.checkpoint,
expected.checkpoint,
check_channel_values=check_channel_values,
)
# Metadata
for k, v in expected.metadata.items():
assert actual.metadata.get(k) == v, (
f"metadata[{k}] mismatch: {actual.metadata.get(k)} != {v}"
)
# Parent config
if expected.parent_config is not None:
assert actual.parent_config is not None, "expected parent_config, got None"
assert (
actual.parent_config["configurable"]["checkpoint_id"]
== expected.parent_config["configurable"]["checkpoint_id"]
), "parent checkpoint_id mismatch"
else:
assert actual.parent_config is None, (
f"expected no parent_config, got {actual.parent_config}"
)
# Pending writes
if check_writes and expected.pending_writes is not None:
assert actual.pending_writes is not None
assert len(actual.pending_writes) == len(expected.pending_writes), (
f"pending_writes length mismatch: {len(actual.pending_writes)} != {len(expected.pending_writes)}"
)
@@ -1,124 +0,0 @@
"""Core conformance runner — detects capabilities, runs test suites, builds report."""
from __future__ import annotations
from langgraph.checkpoint.conformance.capabilities import (
Capability,
DetectedCapabilities,
)
from langgraph.checkpoint.conformance.initializer import RegisteredCheckpointer
from langgraph.checkpoint.conformance.report import (
CapabilityReport,
CapabilityResult,
ProgressCallbacks,
)
from langgraph.checkpoint.conformance.spec.test_copy_thread import run_copy_thread_tests
from langgraph.checkpoint.conformance.spec.test_delete_for_runs import (
run_delete_for_runs_tests,
)
from langgraph.checkpoint.conformance.spec.test_delete_thread import (
run_delete_thread_tests,
)
from langgraph.checkpoint.conformance.spec.test_get_tuple import run_get_tuple_tests
from langgraph.checkpoint.conformance.spec.test_list import run_list_tests
from langgraph.checkpoint.conformance.spec.test_prune import run_prune_tests
from langgraph.checkpoint.conformance.spec.test_put import run_put_tests
from langgraph.checkpoint.conformance.spec.test_put_writes import run_put_writes_tests
# Maps capability to its runner function.
_RUNNERS = {
Capability.PUT: run_put_tests,
Capability.PUT_WRITES: run_put_writes_tests,
Capability.GET_TUPLE: run_get_tuple_tests,
Capability.LIST: run_list_tests,
Capability.DELETE_THREAD: run_delete_thread_tests,
Capability.DELETE_FOR_RUNS: run_delete_for_runs_tests,
Capability.COPY_THREAD: run_copy_thread_tests,
Capability.PRUNE: run_prune_tests,
}
async def validate(
registered: RegisteredCheckpointer,
*,
capabilities: set[str] | None = None,
progress: ProgressCallbacks | None = None,
) -> CapabilityReport:
"""Run the validation suite against a registered checkpointer.
Args:
registered: A RegisteredCheckpointer (from @checkpointer_test decorator).
capabilities: If given, only run tests for these capability names.
Otherwise, auto-detect and run all applicable tests.
progress: Optional progress callbacks for incremental output.
Use ``ProgressCallbacks.default()`` for dot-style,
``ProgressCallbacks.verbose()`` for per-test output, or
``None`` / ``ProgressCallbacks.quiet()`` for silent mode.
Returns:
A CapabilityReport with per-capability results.
"""
report = CapabilityReport(checkpointer_name=registered.name)
# Determine which capabilities to test.
caps_to_test: set[Capability]
if capabilities is not None:
caps_to_test = {Capability(c) for c in capabilities}
else:
caps_to_test = set(Capability)
async with registered.enter_lifespan():
for cap in Capability:
if cap in caps_to_test and cap.value not in registered.skip_capabilities:
# Create a fresh checkpointer for each capability suite.
async with registered.create() as saver:
detected = DetectedCapabilities.from_instance(saver)
is_detected = cap in detected.detected
if not is_detected:
if progress and progress.on_capability_start:
progress.on_capability_start(cap.value, False)
report.results[cap.value] = CapabilityResult(
detected=False,
passed=None,
tests_skipped=1,
)
continue
runner = _RUNNERS.get(cap)
if runner is None:
report.results[cap.value] = CapabilityResult(
detected=True,
passed=None,
tests_skipped=1,
)
continue
if progress and progress.on_capability_start:
progress.on_capability_start(cap.value, True)
passed, failed, failures = await runner(
saver,
on_test_result=progress.on_test_result if progress else None,
)
if progress and progress.on_capability_end:
progress.on_capability_end(cap.value)
report.results[cap.value] = CapabilityResult(
detected=True,
passed=failed == 0,
tests_passed=passed,
tests_failed=failed,
failures=failures,
)
else:
if progress and progress.on_capability_start:
progress.on_capability_start(cap.value, False)
report.results[cap.value] = CapabilityResult(
detected=False,
passed=None,
tests_skipped=1,
)
return report
@@ -1,65 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-conformance"
version = "0.0.1"
description = "Conformance test suite for LangGraph checkpointer implementations."
authors = [{name = "William FH", email = "13333726+hinthornw@users.noreply.github.com"}]
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
dependencies = [
"langgraph-checkpoint>=2.0.0",
]
[project.urls]
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-conformance"
[dependency-groups]
test = [
"pytest",
"pytest-asyncio",
]
lint = [
"ruff",
"ty",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.hatch.build.targets.wheel]
include = ["langgraph"]
[tool.pytest.ini_options]
addopts = "--strict-markers --strict-config --durations=5 -vv"
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.ty.rules]
# The extended methods (acopy_thread, adelete_for_runs, aprune) are checked
# at runtime via capability detection and may not exist on the installed
# base class. Dict literal inference is also overly strict for RunnableConfig.
unresolved-attribute = "ignore"
invalid-argument-type = "ignore"
invalid-return-type = "ignore"
[tool.ruff]
lint.select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
]
lint.ignore = ["E501", "B008"]
target-version = "py310"
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
@@ -1,21 +0,0 @@
"""Self-tests: run the conformance suite against InMemorySaver."""
from __future__ import annotations
import pytest
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.conformance import checkpointer_test, validate
@checkpointer_test(name="InMemorySaver")
async def memory_checkpointer():
yield InMemorySaver()
@pytest.mark.asyncio
async def test_validate_memory_base():
"""InMemorySaver passes all base capability tests."""
report = await validate(memory_checkpointer)
report.print_report()
assert report.passed_all_base(), f"Base tests failed: {report.to_dict()}"
-1124
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9 -12
View File
@@ -1,4 +1,4 @@
.PHONY: test test_watch lint type format
.PHONY: test test_watch lint format
######################
# TESTING AND COVERAGE
@@ -18,7 +18,7 @@ POSTGRES_VERSIONS ?= 15 16
test_pg_version:
@echo "Testing PostgreSQL $(POSTGRES_VERSION)"
@POSTGRES_VERSION=$(POSTGRES_VERSION) make start-postgres
@uv run pytest $(TEST)
@poetry run pytest $(TEST)
@EXIT_CODE=$$?; \
make stop-postgres; \
echo "Finished testing PostgreSQL $(POSTGRES_VERSION); Exit code: $$EXIT_CODE"; \
@@ -36,7 +36,7 @@ test:
TEST ?= .
test_watch:
POSTGRES_VERSION=${POSTGRES_VERSION:-16} make start-postgres; \
uv run ptw $(TEST); \
poetry run ptw $(TEST); \
EXIT_CODE=$$?; \
make stop-postgres; \
exit $$EXIT_CODE
@@ -55,15 +55,12 @@ lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests:
uv run ruff check .
[ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
poetry run ruff check .
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
[ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
type:
mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
[ "$(PYTHON_FILES)" = "" ] || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff:
uv run ruff format $(PYTHON_FILES)
uv run ruff check --select I --fix $(PYTHON_FILES)
poetry run ruff format $(PYTHON_FILES)
poetry run ruff check --select I --fix $(PYTHON_FILES)
+2 -56
View File
@@ -13,20 +13,6 @@ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) withou
> [!IMPORTANT]
> When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
>
> **Why these parameters are required:**
> - `autocommit=True`: Required for the `.setup()` method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.
> - `row_factory=dict_row`: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g., `row["column_name"]`). The default `tuple_row` factory returns tuples that only support index-based access (e.g., `row[0]`), which will cause `TypeError` exceptions when the checkpointer tries to access columns by name.
>
> **Example of incorrect usage:**
> ```python
> # ❌ This will fail with TypeError during checkpointer operations
> with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row
> checkpointer = PostgresSaver(conn)
> checkpointer.setup() # May not persist tables properly
> # Any operation that reads from database will fail with:
> # TypeError: tuple indices must be integers or slices, not str
> ```
```python
from langgraph.checkpoint.postgres import PostgresSaver
@@ -39,7 +25,7 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# call .setup() the first time you're using the checkpointer
checkpointer.setup()
checkpoint = {
"v": 4,
"v": 1,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
@@ -61,6 +47,7 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
"start:node": 2
}
},
"pending_sends": [],
}
# store checkpoint
@@ -72,44 +59,3 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# list checkpoints
list(checkpointer.list(read_config))
```
### Async
```python
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpoint = {
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
}
# store checkpoint
await checkpointer.aput(write_config, checkpoint, {}, {})
# load checkpoint
await checkpointer.aget(read_config)
# list checkpoints
[c async for c in checkpointer.alist(read_config)]
```

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