mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-27 01:52:25 +02:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9b0a686e | ||
|
|
9e02a23682 | ||
|
|
5e70e6f307 | ||
|
|
eb57c06896 | ||
|
|
9284b57ba0 | ||
|
|
25fea591b5 | ||
|
|
b9fe53777f | ||
|
|
35c2e8a679 | ||
|
|
e0fb56c6a3 | ||
|
|
3458a3cecb |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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') }}
|
||||
@@ -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:
|
||||
- "*"
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"aliveStatusCodes": [200, 206, 402],
|
||||
"ignorePatterns": ["*dcbadge.vercel.app*"]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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 user’s *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 it’s 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 user’s 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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||

|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||
[](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>
|
||||
|
||||
## LangGraph’s 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 +0,0 @@
|
||||
_site/
|
||||
@@ -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()
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
-250
@@ -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
|
||||
-218
@@ -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
|
||||
-149
@@ -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()}"
|
||||
Generated
-1124
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user