Compare commits

..
Author SHA1 Message Date
David Asamu 0c850a5baa feat: implement remote build support and archive creation for deployments
- Added a new `create_archive` function to generate a tarball of the project source, including handling of .dockerignore patterns.
- Enhanced the `deploy` function to support remote builds, allowing deployment without local Docker availability.
- Introduced a `--remote` option in the CLI to control remote build behavior.
- Updated the HostBackendClient with methods for requesting upload URLs and updating deployments with source tarballs.
2026-03-11 16:08:13 +00:00
hari-dhanushkodi 19db4d2b23 update docker build logic 2026-03-02 22:43:52 -05:00
David Asamu e6fe1a5d68 update default host backend url 2026-02-26 23:20:34 +00:00
David Asamu aceeb8e352 feat: set default host URL for CLI and simplify deployment error handling
- Added a default value for the `--host-url` option in the CLI to streamline user experience.
- Removed the explicit error check for `host_url` in the `deploy` function, allowing for a more flexible deployment process.
2026-02-26 22:24:29 +00:00
David Asamu 6c2154047a feat: add deployment type option to CLI and enhance deployment process
- Introduced a new CLI option `--deployment-type` to specify the deployment environment (dev or prod).
- Updated the `deploy` function to utilize the specified deployment type when creating a deployment.
- Improved the build process with a progress indicator for better user feedback during deployment.
2026-02-26 22:00:59 +00:00
David Asamu 817125ae4f feat: enhance CLI with environment variable handling and Docker configuration
- Added functions to parse .env files and resolve environment variables from configuration.
- Introduced a context manager for temporary Docker configuration using authentication tokens.
- Updated Docker capabilities check to include optional requirements for Docker Compose and Buildx.
- Enhanced the HostBackendClient with new methods for listing and retrieving deployments and revisions.
- Improved the Progress class to display elapsed time during tasks.
2026-02-26 16:34:08 +00:00
hari-dhanushkodi 9cda4fa11d remove subgraph 2026-02-24 23:23:27 -05:00
hari-dhanushkodi fc9c710bae feat: add langgraph deploy command 2026-02-24 23:22:19 -05:00
176 changed files with 14537 additions and 22176 deletions
+6
View File
@@ -0,0 +1,6 @@
# Contributing to LangGraph
Hi there! Thank you for even being interested in contributing to LangGraph.
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether they involve new features, improved infrastructure, better documentation, or bug fixes.
To learn how to contribute to LangGraph, please follow the [contribution guide here](https://docs.langchain.com/oss/python/contributing).
+24 -48
View File
@@ -1,60 +1,43 @@
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 LangChain Forum at forum.langchain.com.
labels: [pending, bug]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a bug report.
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/).
Use this to report BUGS in LangGraph. 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:
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:
* [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/),
* [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
* [LangChain documentation with the integrated search](https://docs.langchain.com/),
* [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 the LangChain Forum (https://forum.langchain.com/).
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
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. Replace this code with your own!
placeholder: |
from langgraph.graph import StateGraph
@@ -63,13 +46,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 +77,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()
```
Run on your machine: `python -m langchain_core.sys_info`
placeholder: |
python -m langchain_core.sys_info
validations:
+4 -10
View File
@@ -1,15 +1,9 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: 💬 LangChain Forum
- name: Documentation
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
about: Report an issue related to the LangGraph documentation
- name: LangChain Forum
url: https://forum.langchain.com/
about: General community discussions and support
- 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
+1 -1
View File
@@ -21,7 +21,7 @@ Thank you for contributing to LangGraph! Follow these steps to mark your pull re
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.
- [ ] **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://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md) for more.
Additional guidelines:
+9 -102
View File
@@ -4,108 +4,15 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "monday"
groups:
all-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/libs/checkpoint"
- package-ecosystem: "pip"
directories:
- "libs/checkpoint"
- "libs/checkpoint-postgres"
- "libs/checkpoint-sqlite"
- "libs/cli"
- "libs/langgraph"
- "libs/prebuilt"
- "libs/sdk-py"
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:
- "*"
+2 -2
View File
@@ -63,7 +63,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
try:
sys.stderr.write("\n== docker compose ps ==\n")
runner.run(
subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=True)
subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False)
)
except Exception:
pass
@@ -76,7 +76,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
"logs",
"langgraph-api",
input=stdin,
verbose=True,
verbose=False,
)
)
except Exception:
+10 -28
View File
@@ -2,9 +2,6 @@ name: CLI integration test
on:
workflow_call:
secrets:
LANGSMITH_API_KEY:
required: false
permissions:
contents: read
@@ -52,22 +49,19 @@ jobs:
- 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 ${{ matrix.example.name }}
if: steps.changed-files.outputs.all
working-directory: ${{ matrix.example.workdir }}
run: |
langgraph build -t ${{ matrix.example.tag }}
- name: Test service ${{ matrix.example.name }}
if: ${{ steps.changed-files.outputs.all && secrets.LANGSMITH_API_KEY != '' }}
working-directory: ${{ matrix.example.workdir }}
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
# Build the image for this example
langgraph build -t ${{ matrix.example.tag }}
# Prepare environment file from local or parent example directory
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi; fi
# Run the integration test using the built tag
# Compute repo root to reference the shared script robustly
REPO_ROOT=$(git rev-parse --show-toplevel)
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
@@ -88,34 +82,22 @@ jobs:
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' && secrets.LANGSMITH_API_KEY != '' }}
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
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env; fi
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build prerelease reqs service
- name: Build and test 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' && secrets.LANGSMITH_API_KEY != '' }}
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
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
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"
if [ "$LANGGRAPH_VERSION" != "1.0.2" ]; then
echo "LANGGRAPH_VERSION != 1.0.2; $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);")
+1 -3
View File
@@ -39,7 +39,6 @@ jobs:
- 'libs/checkpoint/**'
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/checkpoint-conformance/**'
- 'libs/prebuilt/**'
deps:
- '**/pyproject.toml'
@@ -58,7 +57,7 @@ jobs:
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/checkpoint-conformance",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
@@ -78,7 +77,6 @@ jobs:
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/checkpoint-conformance",
"libs/prebuilt",
"libs/sdk-py",
]
-2
View File
@@ -53,5 +53,3 @@ 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.
-2
View File
@@ -53,5 +53,3 @@ 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 -1
View File
@@ -1,3 +1,3 @@
# 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.
This directory should NOT be used for documentation. All new documentation must be added to `docs/docs/` directory.
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "23544406",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/async.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "14f7ca50",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/branching.ipynb"
]
}
],
"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.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb"
]
}
],
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb"
]
}
],
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb"
]
}
],
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "2b789e16",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/cloud/how-tos/langgraph_to_langgraph_cloud.ipynb"
]
}
],
"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
}
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb"
]
}
],
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1d38cbab",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"15d3ac32-cdf3-4800-a30c-f26d828d69c8.png": {
@@ -41,9 +33,7 @@
"id": "e501686f-323f-4b87-8f9c-8ba89133078b",
"metadata": {},
"outputs": [],
"source": [
"! pip install -U langchain_community langchain-mistralai langchain langgraph"
]
"source": ["! pip install -U langchain_community langchain-mistralai langchain langgraph"]
},
{
"cell_type": "markdown",
@@ -61,12 +51,7 @@
"id": "982e4609-86e4-4934-828f-e03d89c20393",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\n",
"mistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set"
]
"source": ["import os\n\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\nmistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set"]
},
{
"cell_type": "markdown",
@@ -84,12 +69,7 @@
"id": "37b172d2-3a9d-49a8-898c-22ed0cb45c88",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\""
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\""]
},
{
"cell_type": "markdown",
@@ -107,42 +87,7 @@
"id": "a188c8ca-c053-4e6d-b7af-38a3b6b371c7",
"metadata": {},
"outputs": [],
"source": [
"# Select LLM\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_mistralai import ChatMistralAI\n",
"\n",
"mistral_model = \"mistral-large-latest\"\n",
"llm = ChatMistralAI(model=mistral_model, temperature=0)\n",
"\n",
"# Prompt\n",
"code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n",
" defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n",
" \\n Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
" \"\"\"Code output\"\"\"\n",
"\n",
" prefix: str = Field(description=\"Description of the problem and approach\")\n",
" imports: str = Field(description=\"Code block import statements\")\n",
" code: str = Field(description=\"Code block not including import statements\")\n",
" description = \"Schema for code solutions to questions about LCEL.\"\n",
"\n",
"\n",
"# LLM\n",
"code_gen_chain = llm.with_structured_output(code, include_raw=False)"
]
"source": ["# Select LLM\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_mistralai import ChatMistralAI\n\nmistral_model = \"mistral-large-latest\"\nllm = ChatMistralAI(model=mistral_model, temperature=0)\n\n# Prompt\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\ncode_gen_chain = llm.with_structured_output(code, include_raw=False)"]
},
{
"cell_type": "code",
@@ -150,10 +95,7 @@
"id": "9fc0290d-5a04-4514-8664-91f9dbf2da7b",
"metadata": {},
"outputs": [],
"source": [
"question = \"Write a function for fibonacci.\"\n",
"messages = [(\"user\", question)]"
]
"source": ["question = \"Write a function for fibonacci.\"\nmessages = [(\"user\", question)]"]
},
{
"cell_type": "code",
@@ -172,11 +114,7 @@
"output_type": "execute_result"
}
],
"source": [
"# Test\n",
"result = code_gen_chain.invoke(messages)\n",
"result"
]
"source": ["# Test\nresult = code_gen_chain.invoke(messages)\nresult"]
},
{
"cell_type": "markdown",
@@ -192,28 +130,7 @@
"id": "183d77b8-f180-4815-b39f-8ef507ec0534",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated, TypedDict\n",
"\n",
"from langgraph.graph.message import AnyMessage, add_messages\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" error : Binary flag for control flow to indicate whether test error was tripped\n",
" messages : With user question, error messages, reasoning\n",
" generation : Code solution\n",
" iterations : Number of tries\n",
" \"\"\"\n",
"\n",
" error: str\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
" generation: str\n",
" iterations: int"
]
"source": ["from typing import Annotated, TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: Annotated[list[AnyMessage], add_messages]\n generation: str\n iterations: int"]
},
{
"cell_type": "markdown",
@@ -229,163 +146,7 @@
"id": "14bc89d1-3ca6-4847-a048-1803e0e4600e",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"### Parameters\n",
"max_iterations = 3\n",
"\n",
"\n",
"### Nodes\n",
"def generate(state: GraphState):\n",
" \"\"\"\n",
" Generate a code solution\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation\n",
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" # Solution\n",
" code_solution = code_gen_chain.invoke(messages)\n",
" messages += [\n",
" (\n",
" \"assistant\",\n",
" f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n",
" )\n",
" ]\n",
"\n",
" # Increment\n",
" iterations = iterations + 1\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"def code_check(state: GraphState):\n",
" \"\"\"\n",
" Check code\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, error\n",
" \"\"\"\n",
"\n",
" print(\"---CHECKING CODE---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" code_solution = state[\"generation\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" # Get solution components\n",
" imports = code_solution.imports\n",
" code = code_solution.code\n",
"\n",
" # Check imports\n",
" try:\n",
" exec(imports)\n",
" except Exception as e:\n",
" print(\"---CODE IMPORT CHECK: FAILED---\")\n",
" error_message = [\n",
" (\n",
" \"user\",\n",
" f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # Check execution\n",
" try:\n",
" combined_code = f\"{imports}\\n{code}\"\n",
" print(f\"CODE TO TEST: {combined_code}\")\n",
" # Use a shared scope for exec\n",
" global_scope = {}\n",
" exec(combined_code, global_scope)\n",
" except Exception as e:\n",
" print(\"---CODE BLOCK CHECK: FAILED---\")\n",
" error_message = [\n",
" (\n",
" \"user\",\n",
" f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # No errors\n",
" print(\"---NO CODE TEST FAILURES---\")\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"no\",\n",
" }\n",
"\n",
"\n",
"### Conditional edges\n",
"\n",
"\n",
"def decide_to_finish(state: GraphState):\n",
" \"\"\"\n",
" Determines whether to finish.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Next node to call\n",
" \"\"\"\n",
" error = state[\"error\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" if error == \"no\" or iterations == max_iterations:\n",
" print(\"---DECISION: FINISH---\")\n",
" return \"end\"\n",
" else:\n",
" print(\"---DECISION: RE-TRY SOLUTION---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"### Utilities\n",
"\n",
"\n",
"def _print_event(event: dict, _printed: set, max_length=1500):\n",
" current_state = event.get(\"dialog_state\")\n",
" if current_state:\n",
" print(\"Currently in: \", current_state[-1])\n",
" message = event.get(\"messages\")\n",
" if message:\n",
" if isinstance(message, list):\n",
" message = message[-1]\n",
" if message.id not in _printed:\n",
" msg_repr = message.pretty_repr(html=True)\n",
" if len(msg_repr) > max_length:\n",
" msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n",
" print(msg_repr)\n",
" _printed.add(message.id)"
]
"source": ["import uuid\n\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameters\nmax_iterations = 3\n\n\n### Nodes\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n\n # Solution\n code_solution = code_gen_chain.invoke(messages)\n messages += [\n (\n \"assistant\",\n f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n combined_code = f\"{imports}\\n{code}\"\n print(f\"CODE TO TEST: {combined_code}\")\n # Use a shared scope for exec\n global_scope = {}\n exec(combined_code, global_scope)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\n### Conditional edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n return \"generate\"\n\n\n### Utilities\n\n\ndef _print_event(event: dict, _printed: set, max_length=1500):\n current_state = event.get(\"dialog_state\")\n if current_state:\n print(\"Currently in: \", current_state[-1])\n message = event.get(\"messages\")\n if message:\n if isinstance(message, list):\n message = message[-1]\n if message.id not in _printed:\n msg_repr = message.pretty_repr(html=True)\n if len(msg_repr) > max_length:\n msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n print(msg_repr)\n _printed.add(message.id)"]
},
{
"cell_type": "code",
@@ -393,31 +154,7 @@
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"builder = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"builder.add_node(\"generate\", generate) # generation solution\n",
"builder.add_node(\"check_code\", code_check) # check code\n",
"\n",
"# Build graph\n",
"builder.add_edge(START, \"generate\")\n",
"builder.add_edge(\"generate\", \"check_code\")\n",
"builder.add_conditional_edges(\n",
" \"check_code\",\n",
" decide_to_finish,\n",
" {\n",
" \"end\": END,\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"\n",
"memory = InMemorySaver()\n",
"graph = builder.compile(checkpointer=memory)"
]
"source": ["from langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = InMemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
@@ -436,15 +173,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -452,23 +181,7 @@
"id": "242aa2f0-2c31-462f-a958-ff9ae0cf7c62",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"Write a Python program that prints 'Hello, World!' to the console.\"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"Write a Python program that prints 'Hello, World!' to the console.\"\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -486,31 +199,7 @@
"id": "390b2768-f395-4aea-8b0e-9d36212a31ac",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n",
"\n",
"Requirements:\n",
"The program should define a function is_palindrome(s) that takes a string s as input.\n",
"The function should return True if the string is a palindrome and False otherwise.\n",
"Ignore spaces, punctuation, and case differences when checking for palindromes.\n",
"\n",
"Give an example of it working on an example input word.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n\nRequirements:\nThe program should define a function is_palindrome(s) that takes a string s as input.\nThe function should return True if the string is a palindrome and False otherwise.\nIgnore spaces, punctuation, and case differences when checking for palindromes.\n\nGive an example of it working on an example input word.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -528,26 +217,7 @@
"id": "0a3f946b-e2f2-44d9-905b-09f36980cf9f",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Write a program that prints the numbers from 1 to 100. \n",
"But for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \n",
"For numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Write a program that prints the numbers from 1 to 100. \nBut for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \nFor numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -565,37 +235,7 @@
"id": "2bb883df-540b-46ab-9415-fe27db68456f",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"I want to vectorize a function\n",
"\n",
" frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n",
" for i, val1 in enumerate(rows):\n",
" for j, val2 in enumerate(cols):\n",
" for j, val3 in enumerate(ch):\n",
" # Assuming you want to store the pair as tuples in the matrix\n",
" frame[i, j, k] = image[val1, val2, val3]\n",
"\n",
" out.write(np.array(frame))\n",
"\n",
"with a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["import uuid\n\n_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"I want to vectorize a function\n\n frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n for i, val1 in enumerate(rows):\n for j, val2 in enumerate(cols):\n for j, val3 in enumerate(ch):\n # Assuming you want to store the pair as tuples in the matrix\n frame[i, j, k] = image[val1, val2, val3]\n\n out.write(np.array(frame))\n\nwith a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -613,34 +253,7 @@
"id": "ee05da1f-c272-405d-8a7b-552cfc3106e1",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n",
"\n",
"- Allow players to take turns to input their moves.\n",
"- Check for invalid moves (e.g., placing a marker on an already occupied space).\n",
"- Determine and announce the winner or if the game ends in a draw.\n",
"\n",
"Requirements:\n",
"- Use a 2D list to represent the Tic-Tac-Toe board.\n",
"- Use functions to modularize the code.\n",
"- Validate player input.\n",
"- Check for win conditions and draw conditions after each move.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n\n- Allow players to take turns to input their moves.\n- Check for invalid moves (e.g., placing a marker on an already occupied space).\n- Determine and announce the winner or if the game ends in a draw.\n\nRequirements:\n- Use a 2D list to represent the Tic-Tac-Toe board.\n- Use functions to modularize the code.\n- Validate player input.\n- Check for win conditions and draw conditions after each move.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -658,7 +271,7 @@
"id": "814fc2a4-8e5b-4faa-8f52-3977226bd09a",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e9a58c69",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/configuration.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a1e6efeb",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/create-react-agent-hitl.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1ef41a89",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/create-react-agent-memory.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9e2f7902",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/create-react-agent-system-prompt.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "eb07372e",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/create-react-agent.ipynb"
]
}
],
"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
}
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/customer-support/customer-support.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/extraction/retries.ipynb"
]
}
],
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fc0793cb",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/input_output_schema.ipynb"
]
}
],
"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 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/lats/lats.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "42abb708",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/map-reduce.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "298784f6",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-summary-conversation-history.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3f4370fd",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/delete-messages.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "6ec7cb13",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/manage-conversation-history.ipynb"
]
}
],
"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": 5
}
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb"
]
}
],
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/multi_agent/multi-agent-collaboration.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "017a01f4",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/node-retries.ipynb"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env",
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "05f6ad0a",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/pass-config-to-tools.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8f38bec5",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/pass-run-time-values-to-tools.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "4da17088",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/pass_private_state.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d16e8b9c",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md."
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "78217098",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_mongodb.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "18526f23",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "eee6ecdd",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_redis.ipynb"
]
}
],
"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
}
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb"
]
}
],
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fedd6d23",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"36fa621a-9d3d-4860-a17c-5d20e6987481.png": {
File diff suppressed because one or more lines are too long
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "39b26b09",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"3755396d-c4a8-45bd-87d4-00cb56339fe5.png": {
+3 -11
View File
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "47e3b43b",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"cell_type": "markdown",
"id": "425fb020-e864-40ce-a31f-8da40c73d14b",
@@ -208,11 +200,11 @@
"output_type": "stream",
"text": [
"********************Prompt[rlm/rag-prompt]********************\n",
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"================================\u001B[1m Human Message \u001B[0m=================================\n",
"\n",
"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\n",
"Question: \u001b[33;1m\u001b[1;3m{question}\u001b[0m \n",
"Context: \u001b[33;1m\u001b[1;3m{context}\u001b[0m \n",
"Question: \u001B[33;1m\u001B[1;3m{question}\u001B[0m \n",
"Context: \u001B[33;1m\u001B[1;3m{context}\u001B[0m \n",
"Answer:\n"
]
}
-8
View File
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "c71da2ea",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"683fae34-980f-43f0-a9c2-9894bebd9157.png": {
-8
View File
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ac7db067",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"b77a7d3b-b28a-4dcf-9f1a-861f2f2c5f6c.png": {
-8
View File
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b3d959ff",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"15cba0ab-a549-4909-8373-fb761e384eff.png": {
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "345488d8",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"5fca0a3e-d13d-4bfa-95ea-58203640cc7a.png": {
@@ -1,13 +1,5 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "403aeb6e",
"metadata": {},
"source": [
"This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
]
},
{
"attachments": {
"15cba0ab-a549-4909-8373-fb761e384eff.png": {
@@ -62,11 +54,7 @@
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
"import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
},
{
@@ -76,9 +64,7 @@
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
"import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
]
},
{
@@ -98,18 +84,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import OpenAIEmbeddings\n",
"from langchain_pinecone import PineconeVectorStore\n",
"\n",
"# use pinecone movies database\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = PineconeVectorStore(\n",
" embedding=OpenAIEmbeddings(),\n",
" index_name=\"sample-movies\",\n",
" text_key=\"summary\",\n",
")\n",
"retriever = vectorstore.as_retriever()"
"from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"
]
},
{
@@ -138,11 +113,7 @@
}
],
"source": [
"docs = retriever.invoke(\"James Cameron\")\n",
"for doc in docs:\n",
" print(\"# \" + doc.metadata[\"title\"])\n",
" print(doc.page_content)\n",
" print()"
"docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"
]
},
{
@@ -202,12 +173,7 @@
}
],
"source": [
"# Test the retrieval grader\n",
"question = \"movies starring jason momoa\"\n",
"docs = retriever.invoke(question)\n",
"doc_txt = docs[0].page_content\n",
"print(doc_txt)\n",
"print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
"# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
},
{
@@ -235,23 +201,7 @@
}
],
"source": [
"### Generate\n",
"\n",
"from langchain import hub\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
"print(generation)"
"### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
]
},
{
@@ -379,17 +329,7 @@
}
],
"source": [
"### Question Re-writer\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt\n",
"re_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n",
"\n",
"question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
"print(question)\n",
"question_rewriter.invoke({\"question\": question})"
"### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"
]
},
{
@@ -411,24 +351,7 @@
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
"from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"
]
},
{
@@ -438,95 +361,7 @@
"metadata": {},
"outputs": [],
"source": [
"### Nodes\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" print(\"---RETRIEVE---\")\n",
" question = state[\"question\"]\n",
"\n",
" # Retrieval\n",
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
" else:\n",
" print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates question key with a re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}"
"### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}"
]
},
{
@@ -536,74 +371,7 @@
"metadata": {},
"outputs": [],
"source": [
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---ASSESS GRADED DOCUMENTS---\")\n",
" state[\"question\"]\n",
" filtered_documents = state[\"documents\"]\n",
"\n",
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"def grade_generation_v_documents_and_question(state):\n",
" \"\"\"\n",
" Determines whether the generation is grounded in the document and answers question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK HALLUCINATIONS---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
" # Check question-answering\n",
" print(\"---GRADE GENERATION vs QUESTION---\")\n",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
" else:\n",
" print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
" return \"not useful\"\n",
" else:\n",
" pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\""
"### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""
]
},
{
@@ -622,42 +390,7 @@
"id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generate\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.add_edge(START, \"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"transform_query\": \"transform_query\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"transform_query\", \"retrieve\")\n",
"workflow.add_conditional_edges(\n",
" \"generate\",\n",
" grade_generation_v_documents_and_question,\n",
" {\n",
" \"not supported\": \"generate\",\n",
" \"useful\": END,\n",
" \"not useful\": \"transform_query\",\n",
" },\n",
")\n",
"\n",
"# Compile\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generate\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -693,18 +426,7 @@
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"Movies that star Daniel Craig\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
"from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
]
},
{
@@ -714,15 +436,7 @@
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"question\": \"Which movies are about aliens?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
"inputs = {\"question\": \"Which movies are about aliens?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
]
},
{
@@ -731,7 +445,9 @@
"id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
"metadata": {},
"outputs": [],
"source": []
"source": [
""
]
}
],
"metadata": {
+1 -8
View File
@@ -5,14 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/react-agent-from-scratch.ipynb"
]
}
],
+1 -8
View File
@@ -5,14 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/react-agent-structured-output.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fa3f7c50",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/recursion-limit.ipynb"
]
}
],
"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 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/reflection/reflection.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/reflexion/reflexion.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/rewoo/rewoo.ipynb"
]
}
],
+1 -8
View File
@@ -5,14 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/run-id-langsmith.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/self-discover/self-discover.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "4149ffcc",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/state-model.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3e05d7f9",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/storm/storm.ipynb"
]
}
],
"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": 4
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e663f597",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/stream-multiple.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e6829c80",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/stream-updates.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "5ec11895",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/stream-values.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "6619387c",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-content.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "57b7e303",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-events-from-within-tools-without-langchain.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8e71a0c8",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-events-from-within-tools.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "756e4554",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-from-final-node.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "47164a72",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-subgraphs.ipynb"
]
}
],
"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
}
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "218dfbcb",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-tokens-without-langchain.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "99eb887e",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/streaming-tokens.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0de7689f",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/subgraph-transform-state.ipynb"
]
}
],
"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 -8
View File
@@ -5,14 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/subgraph.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "5106959e",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/subgraphs-manage-state.ipynb"
]
}
],
"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
}
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "dc21501d",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/tool-calling-errors.ipynb"
]
}
],
"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 -8
View File
@@ -5,14 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/tool-calling.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/sql-agent.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb"
]
}
],
+1 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/usaco/usaco.ipynb"
]
}
],
+33
View File
@@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9c9cb15a",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/visualization.ipynb"
]
}
],
"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 -9
View File
@@ -5,15 +5,7 @@
"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)."
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/web-navigation/web_voyager.ipynb"
]
}
],
-12
View File
@@ -1,12 +0,0 @@
.PHONY: format lint test
format:
uv run ruff format .
uv run ruff check --fix .
lint:
uv run ruff check .
uv run ty check
test:
uv run pytest $(TEST)
-111
View File
@@ -1,111 +0,0 @@
# langgraph-checkpoint-conformance
Conformance test suite for [LangGraph](https://github.com/langchain-ai/langgraph) checkpointer implementations.
Validates that a `BaseCheckpointSaver` subclass correctly implements the checkpoint storage contract — blob round-trips, metadata preservation, namespace isolation, incremental channel updates, and more.
## Installation
```bash
pip install langgraph-checkpoint-conformance
```
## Quick start
Register your checkpointer with `@checkpointer_test` and run `validate()`:
```python
import asyncio
from langgraph.checkpoint.conformance import checkpointer_test, validate
@checkpointer_test(name="MyCheckpointer")
async def my_checkpointer():
saver = MyCheckpointer(...)
yield saver
# cleanup runs after yield
async def main():
report = await validate(my_checkpointer)
report.print_report()
assert report.passed_all_base()
asyncio.run(main())
```
Or in a pytest test:
```python
import pytest
from langgraph.checkpoint.conformance import checkpointer_test, validate
@checkpointer_test(name="MyCheckpointer")
async def my_checkpointer():
yield MyCheckpointer(...)
@pytest.mark.asyncio
async def test_conformance():
report = await validate(my_checkpointer)
report.print_report()
assert report.passed_all_base()
```
## Capabilities
The suite tests **base** capabilities (required) and **extended** capabilities (optional, auto-detected):
| Capability | Required | Method |
|---|---|---|
| `put` | yes | `aput` |
| `put_writes` | yes | `aput_writes` |
| `get_tuple` | yes | `aget_tuple` |
| `list` | yes | `alist` |
| `delete_thread` | yes | `adelete_thread` |
| `delete_for_runs` | no | `adelete_for_runs` |
| `copy_thread` | no | `acopy_thread` |
| `prune` | no | `aprune` |
Extended capabilities are detected by checking whether the method is overridden from `BaseCheckpointSaver`. If not overridden, those tests are skipped.
## Options
### Progress output
```python
from langgraph.checkpoint.conformance.report import ProgressCallbacks
# Dot-style progress (. per pass, F per fail)
report = await validate(my_checkpointer, progress=ProgressCallbacks.default())
# Verbose (per-test names + stacktraces on failure)
report = await validate(my_checkpointer, progress=ProgressCallbacks.verbose())
```
### Skip capabilities
```python
@checkpointer_test(name="MyCheckpointer", skip_capabilities={"prune"})
async def my_checkpointer():
yield MyCheckpointer(...)
```
### Run specific capabilities
```python
report = await validate(my_checkpointer, capabilities={"put", "list"})
```
### Lifespan (one-time setup/teardown)
For expensive setup like database creation:
```python
async def db_lifespan():
await create_database()
yield
await drop_database()
@checkpointer_test(name="PostgresSaver", lifespan=db_lifespan)
async def pg_checkpointer():
async with PostgresSaver.from_conn_string(CONN_STRING) as saver:
yield saver
```
@@ -1,9 +0,0 @@
"""langgraph-checkpoint-conformance: conformance test suite for checkpointer implementations."""
from langgraph.checkpoint.conformance.initializer import checkpointer_test
from langgraph.checkpoint.conformance.validate import validate
__all__ = [
"checkpointer_test",
"validate",
]
@@ -1,93 +0,0 @@
"""Capability detection for checkpointer implementations."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING
from langgraph.checkpoint.base import BaseCheckpointSaver
if TYPE_CHECKING:
pass
class Capability(str, Enum):
"""Capabilities that a checkpointer may support."""
PUT = "put"
PUT_WRITES = "put_writes"
GET_TUPLE = "get_tuple"
LIST = "list"
DELETE_THREAD = "delete_thread"
DELETE_FOR_RUNS = "delete_for_runs"
COPY_THREAD = "copy_thread"
PRUNE = "prune"
# Capabilities that every checkpointer must support.
BASE_CAPABILITIES = frozenset(
{
Capability.PUT,
Capability.PUT_WRITES,
Capability.GET_TUPLE,
Capability.LIST,
Capability.DELETE_THREAD,
}
)
# Capabilities that are optional extensions.
EXTENDED_CAPABILITIES = frozenset(
{
Capability.DELETE_FOR_RUNS,
Capability.COPY_THREAD,
Capability.PRUNE,
}
)
ALL_CAPABILITIES = BASE_CAPABILITIES | EXTENDED_CAPABILITIES
# Maps capability to the async method name on BaseCheckpointSaver (or subclass).
_CAPABILITY_METHOD_MAP: dict[Capability, str] = {
Capability.PUT: "aput",
Capability.PUT_WRITES: "aput_writes",
Capability.GET_TUPLE: "aget_tuple",
Capability.LIST: "alist",
Capability.DELETE_THREAD: "adelete_thread",
Capability.DELETE_FOR_RUNS: "adelete_for_runs",
Capability.COPY_THREAD: "acopy_thread",
Capability.PRUNE: "aprune",
}
@dataclass(frozen=True)
class DetectedCapabilities:
"""Result of capability detection for a checkpointer type."""
detected: frozenset[Capability]
missing: frozenset[Capability]
@classmethod
def from_instance(cls, saver: BaseCheckpointSaver) -> DetectedCapabilities:
"""Detect capabilities from a checkpointer instance."""
inner_type = type(saver)
detected: set[Capability] = set()
for cap, method_name in _CAPABILITY_METHOD_MAP.items():
if _is_overridden(inner_type, method_name):
detected.add(cap)
detected_fs = frozenset(detected)
return cls(
detected=detected_fs,
missing=ALL_CAPABILITIES - detected_fs,
)
def _is_overridden(inner_type: type, method: str) -> bool:
"""Check if *method* on *inner_type* differs from the base class default."""
base = getattr(BaseCheckpointSaver, method, None)
impl = getattr(inner_type, method, None)
if base is None or impl is None:
return impl is not None
return impl is not base
@@ -1,100 +0,0 @@
"""Checkpointer test registration and factory management."""
from __future__ import annotations
from collections.abc import AsyncGenerator, Callable
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any
from langgraph.checkpoint.base import BaseCheckpointSaver
# Type for the lifespan async context manager factory.
LifespanFactory = Callable[[], AsyncGenerator[None, None]]
# Module-level registry of decorated checkpointer factories.
_REGISTRY: dict[str, RegisteredCheckpointer] = {}
async def _noop_lifespan() -> AsyncGenerator[None, None]:
yield
@dataclass
class RegisteredCheckpointer:
"""A registered checkpointer test factory."""
name: str
factory: Callable[[], AsyncGenerator[BaseCheckpointSaver, None]]
skip_capabilities: set[str] = field(default_factory=set)
lifespan: LifespanFactory = _noop_lifespan
@asynccontextmanager
async def create(self) -> AsyncGenerator[BaseCheckpointSaver, None]:
"""Create a fresh checkpointer instance via the async generator."""
gen = self.factory()
try:
saver = await gen.__anext__()
yield saver
finally:
try:
await gen.__anext__()
except StopAsyncIteration:
pass
@asynccontextmanager
async def enter_lifespan(self) -> AsyncGenerator[None, None]:
"""Enter the lifespan context (once per validation run)."""
gen = self.lifespan()
try:
await gen.__anext__()
yield
finally:
try:
await gen.__anext__()
except StopAsyncIteration:
pass
def checkpointer_test(
name: str,
*,
skip_capabilities: set[str] | None = None,
lifespan: LifespanFactory | None = None,
) -> Callable[[Any], RegisteredCheckpointer]:
"""Register an async generator as a checkpointer test factory.
The factory is called once per capability suite to create a fresh
checkpointer. The optional `lifespan` is an async generator that
runs once for the entire validation run (e.g. to create/destroy a
database).
Example::
@checkpointer_test(name="InMemorySaver")
async def memory_checkpointer():
yield InMemorySaver()
With lifespan::
async def pg_lifespan():
await create_database()
yield
await drop_database()
@checkpointer_test(name="PostgresSaver", lifespan=pg_lifespan)
async def pg_checkpointer():
yield PostgresSaver(conn_string="...")
"""
def decorator(fn: Any) -> RegisteredCheckpointer:
registered = RegisteredCheckpointer(
name=name,
factory=fn,
skip_capabilities=skip_capabilities or set(),
lifespan=lifespan or _noop_lifespan,
)
_REGISTRY[name] = registered
return registered
return decorator
@@ -1,198 +0,0 @@
"""Capability report: results, progress callbacks, and pretty-printing."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from langgraph.checkpoint.conformance.capabilities import (
BASE_CAPABILITIES,
EXTENDED_CAPABILITIES,
Capability,
)
# Callback type for per-test progress reporting.
# (capability_name, test_name, passed, error_msg_or_None) -> None
OnTestResult = Callable[[str, str, bool, str | None], None]
# Callback type for capability-level events.
# (capability_name, detected) -> None
OnCapabilityStart = Callable[[str, bool], None]
class ProgressCallbacks:
"""Grouped callbacks for progress reporting during validation."""
def __init__(
self,
*,
on_capability_start: Callable[[str, bool], None] | None = None,
on_test_result: OnTestResult | None = None,
on_capability_end: Callable[[str], None] | None = None,
) -> None:
self.on_capability_start = on_capability_start
self.on_test_result = on_test_result
self.on_capability_end = on_capability_end
@classmethod
def default(cls) -> ProgressCallbacks:
"""Dot-style progress: ``.`` per pass, ``F`` per fail."""
def _cap_start(capability: str, detected: bool) -> None:
if detected:
print(f" {capability}: ", end="", flush=True)
else:
print(f"{capability} (not implemented)")
def _test_result(
capability: str, test_name: str, passed: bool, error: str | None
) -> None:
print("." if passed else "F", end="", flush=True)
def _cap_end(capability: str) -> None:
print() # newline after dots
return cls(
on_capability_start=_cap_start,
on_test_result=_test_result,
on_capability_end=_cap_end,
)
@classmethod
def verbose(cls) -> ProgressCallbacks:
"""Per-test output with names and errors."""
def _cap_start(capability: str, detected: bool) -> None:
if detected:
print(f" {capability}:")
else:
print(f"{capability} (not implemented)")
def _test_result(
capability: str, test_name: str, passed: bool, error: str | None
) -> None:
icon = "" if passed else ""
print(f" {icon} {test_name}")
if error:
for line in error.rstrip().splitlines():
print(f" {line}")
return cls(
on_capability_start=_cap_start,
on_test_result=_test_result,
)
@classmethod
def quiet(cls) -> ProgressCallbacks:
"""No progress output."""
return cls()
@dataclass
class CapabilityResult:
"""Result of running a single capability's test suite."""
detected: bool = False
passed: bool | None = None # None = skipped
tests_passed: int = 0
tests_failed: int = 0
tests_skipped: int = 0
failures: list[str] = field(default_factory=list)
@dataclass
class CapabilityReport:
"""Aggregate report across all capabilities."""
checkpointer_name: str
results: dict[str, CapabilityResult] = field(default_factory=dict)
def passed_all_base(self) -> bool:
"""Whether all base capability tests passed."""
for cap in BASE_CAPABILITIES:
result = self.results.get(cap.value)
if result is None or result.passed is not True:
return False
return True
def passed_all(self) -> bool:
"""Whether every detected capability's tests passed."""
for result in self.results.values():
if result.detected and result.passed is not True:
return False
return True
def conformance_level(self) -> str:
"""Return a human-readable conformance level string."""
if self.passed_all():
return "FULL"
if self.passed_all_base():
return "BASE+PARTIAL"
return "BASE" if self._any_base_passed() else "NONE"
def _any_base_passed(self) -> bool:
for cap in BASE_CAPABILITIES:
result = self.results.get(cap.value)
if result and result.passed is True:
return True
return False
def print_report(self) -> None:
"""Pretty-print the report to stdout."""
width = 52
border = "=" * width
print(f"\n{'':>2}{border}")
print(f"{'':>2} Checkpointer Validation: {self.checkpointer_name}")
print(f"{'':>2}{border}")
def _section(title: str, caps: frozenset[Capability]) -> None:
print(f"{'':>2} {title}")
for cap in sorted(caps, key=lambda c: c.value):
result = self.results.get(cap.value)
if result is None:
icon = " "
suffix = "(no tests)"
elif not result.detected:
icon = ""
suffix = "(not implemented)"
elif result.passed is True:
icon = ""
suffix = ""
elif result.passed is False:
icon = ""
suffix = f"({result.tests_failed} failed)"
else:
icon = ""
suffix = "(skipped)"
print(f"{'':>2} {icon} {cap.value:20s} {suffix}")
print()
_section("BASE CAPABILITIES", BASE_CAPABILITIES)
_section("EXTENDED CAPABILITIES", EXTENDED_CAPABILITIES)
total = sum(1 for r in self.results.values() if r.detected)
passed = sum(
1 for r in self.results.values() if r.detected and r.passed is True
)
level = self.conformance_level()
print(f"{'':>2} Result: {level} ({passed}/{total})")
print(f"{'':>2}{border}\n")
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-serializable dict."""
return {
"checkpointer_name": self.checkpointer_name,
"conformance_level": self.conformance_level(),
"results": {
name: {
"detected": r.detected,
"passed": r.passed,
"tests_passed": r.tests_passed,
"tests_failed": r.tests_failed,
"tests_skipped": r.tests_skipped,
"failures": r.failures,
}
for name, r in self.results.items()
},
}
@@ -1,27 +0,0 @@
"""Test spec modules for each checkpointer capability."""
from langgraph.checkpoint.conformance.spec.test_copy_thread import (
run_copy_thread_tests,
)
from langgraph.checkpoint.conformance.spec.test_delete_for_runs import (
run_delete_for_runs_tests,
)
from langgraph.checkpoint.conformance.spec.test_delete_thread import (
run_delete_thread_tests,
)
from langgraph.checkpoint.conformance.spec.test_get_tuple import run_get_tuple_tests
from langgraph.checkpoint.conformance.spec.test_list import run_list_tests
from langgraph.checkpoint.conformance.spec.test_prune import run_prune_tests
from langgraph.checkpoint.conformance.spec.test_put import run_put_tests
from langgraph.checkpoint.conformance.spec.test_put_writes import run_put_writes_tests
__all__ = [
"run_put_tests",
"run_put_writes_tests",
"run_get_tuple_tests",
"run_list_tests",
"run_delete_thread_tests",
"run_delete_for_runs_tests",
"run_copy_thread_tests",
"run_prune_tests",
]
@@ -1,250 +0,0 @@
"""COPY_THREAD capability tests — acopy_thread."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_source_thread(
saver: BaseCheckpointSaver,
tid: str,
*,
n: int = 3,
namespaces: list[str] | None = None,
) -> list[dict]:
"""Create n checkpoints on tid (optionally across namespaces). Returns stored configs."""
nss = namespaces or [""]
stored = []
for ns in nss:
parent_cfg = None
for i in range(n):
config = generate_config(tid, checkpoint_ns=ns)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint(channel_values={"step": i})
cp["channel_versions"] = {"step": 1}
parent_cfg = await saver.aput(
config, cp, generate_metadata(step=i), {"step": 1}
)
stored.append(parent_cfg)
return stored
async def test_copy_thread_basic(saver: BaseCheckpointSaver) -> None:
"""Checkpoints appear on target thread."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src)
await saver.acopy_thread(src, dst)
results = []
async for tup in saver.alist(generate_config(dst)):
results.append(tup)
assert len(results) == 3, f"Expected 3 copied checkpoints, got {len(results)}"
async def test_copy_thread_all_checkpoints(saver: BaseCheckpointSaver) -> None:
"""All checkpoints copied, not just latest."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=3)
await saver.acopy_thread(src, dst)
src_results = []
async for tup in saver.alist(generate_config(src)):
src_results.append(tup)
dst_results = []
async for tup in saver.alist(generate_config(dst)):
dst_results.append(tup)
assert len(dst_results) == len(src_results)
# Verify content matches
for s, d in zip(
sorted(src_results, key=lambda t: t.checkpoint["id"]),
sorted(dst_results, key=lambda t: t.checkpoint["id"]),
strict=True,
):
assert s.checkpoint["channel_values"] == d.checkpoint["channel_values"], (
f"channel_values mismatch for checkpoint {s.checkpoint['id']}"
)
async def test_copy_thread_preserves_metadata(
saver: BaseCheckpointSaver,
) -> None:
"""Metadata intact on copied checkpoints."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=2)
await saver.acopy_thread(src, dst)
src_tuples = []
async for tup in saver.alist(generate_config(src)):
src_tuples.append(tup)
dst_tuples = []
async for tup in saver.alist(generate_config(dst)):
dst_tuples.append(tup)
for s, d in zip(
sorted(src_tuples, key=lambda t: t.metadata.get("step", 0)),
sorted(dst_tuples, key=lambda t: t.metadata.get("step", 0)),
strict=True,
):
for key in s.metadata:
assert s.metadata.get(key) == d.metadata.get(key), (
f"metadata[{key!r}] mismatch: {s.metadata.get(key)!r} != {d.metadata.get(key)!r}"
)
async def test_copy_thread_preserves_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Root + child namespaces copied."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=1, namespaces=["", "child:1"])
await saver.acopy_thread(src, dst)
for ns in ["", "child:1"]:
results = []
async for tup in saver.alist(generate_config(dst, checkpoint_ns=ns)):
results.append(tup)
assert len(results) == 1, (
f"Expected 1 checkpoint in namespace '{ns}', got {len(results)}"
)
async def test_copy_thread_preserves_writes(saver: BaseCheckpointSaver) -> None:
"""Pending writes copied."""
src = str(uuid4())
dst = str(uuid4())
configs = await _setup_source_thread(saver, src, n=1)
# Add a write to the source
await saver.aput_writes(configs[-1], [("ch", "write_val")], str(uuid4()))
await saver.acopy_thread(src, dst)
tup = await saver.aget_tuple(generate_config(dst))
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "write_val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_copy_thread_preserves_ordering(
saver: BaseCheckpointSaver,
) -> None:
"""Checkpoint order maintained."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=4)
await saver.acopy_thread(src, dst)
src_ids = []
async for tup in saver.alist(generate_config(src)):
src_ids.append(tup.checkpoint["id"])
dst_ids = []
async for tup in saver.alist(generate_config(dst)):
dst_ids.append(tup.checkpoint["id"])
# Order should match (both newest-first)
assert src_ids == dst_ids
async def test_copy_thread_source_unchanged(saver: BaseCheckpointSaver) -> None:
"""Source thread still intact after copy."""
src = str(uuid4())
dst = str(uuid4())
await _setup_source_thread(saver, src, n=2)
# Snapshot source before copy
src_before = []
async for tup in saver.alist(generate_config(src)):
src_before.append(tup.checkpoint["id"])
await saver.acopy_thread(src, dst)
# Source should be unchanged
src_after = []
async for tup in saver.alist(generate_config(src)):
src_after.append(tup.checkpoint["id"])
assert src_before == src_after
async def test_copy_thread_nonexistent_source(
saver: BaseCheckpointSaver,
) -> None:
"""Graceful handling of non-existent source thread."""
src = str(uuid4())
dst = str(uuid4())
# Should not raise (or raise a known error)
try:
await saver.acopy_thread(src, dst)
except Exception:
pass # Some implementations may raise; that's acceptable
# Destination should be empty
results = []
async for tup in saver.alist(generate_config(dst)):
results.append(tup)
assert len(results) == 0
ALL_COPY_THREAD_TESTS = [
test_copy_thread_basic,
test_copy_thread_all_checkpoints,
test_copy_thread_preserves_metadata,
test_copy_thread_preserves_namespaces,
test_copy_thread_preserves_writes,
test_copy_thread_preserves_ordering,
test_copy_thread_source_unchanged,
test_copy_thread_nonexistent_source,
]
async def run_copy_thread_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all copy_thread tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_COPY_THREAD_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("copy_thread", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"copy_thread", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,218 +0,0 @@
"""DELETE_FOR_RUNS capability tests — adelete_for_runs."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _put_with_run_id(
saver: BaseCheckpointSaver,
tid: str,
run_id: str,
*,
checkpoint_ns: str = "",
parent_config: dict | None = None,
) -> dict:
"""Put a checkpoint with a run_id in metadata, return stored config."""
config = generate_config(tid, checkpoint_ns=checkpoint_ns)
if parent_config:
config["configurable"]["checkpoint_id"] = parent_config["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
md = generate_metadata(run_id=run_id)
return await saver.aput(config, cp, md, {})
async def test_delete_for_runs_single(saver: BaseCheckpointSaver) -> None:
"""One run_id removed."""
tid = str(uuid4())
run1, run2 = str(uuid4()), str(uuid4())
stored1 = await _put_with_run_id(saver, tid, run1)
await _put_with_run_id(saver, tid, run2, parent_config=stored1)
# Pre-delete: verify both runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, "Pre-delete: run1 should exist"
assert run2 in pre_run_ids, "Pre-delete: run2 should exist"
await saver.adelete_for_runs([run1])
# run1's checkpoint should be gone; run2 should remain
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
assert run2 in run_ids
async def test_delete_for_runs_multiple(saver: BaseCheckpointSaver) -> None:
"""List of run_ids removed."""
tid = str(uuid4())
run1, run2, run3 = str(uuid4()), str(uuid4()), str(uuid4())
s1 = await _put_with_run_id(saver, tid, run1)
s2 = await _put_with_run_id(saver, tid, run2, parent_config=s1)
await _put_with_run_id(saver, tid, run3, parent_config=s2)
# Pre-delete: verify all 3 runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, "Pre-delete: run1 should exist"
assert run2 in pre_run_ids, "Pre-delete: run2 should exist"
assert run3 in pre_run_ids, "Pre-delete: run3 should exist"
await saver.adelete_for_runs([run1, run2])
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
assert run2 not in run_ids
assert run3 in run_ids
async def test_delete_for_runs_preserves_other_runs(
saver: BaseCheckpointSaver,
) -> None:
"""Unrelated runs untouched."""
tid = str(uuid4())
run_keep = str(uuid4())
run_delete = str(uuid4())
await _put_with_run_id(saver, tid, run_keep)
await _put_with_run_id(saver, tid, run_delete)
# Pre-delete: verify both runs exist
pre_results = []
async for tup in saver.alist(generate_config(tid)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run_keep in pre_run_ids, "Pre-delete: run_keep should exist"
assert run_delete in pre_run_ids, "Pre-delete: run_delete should exist"
await saver.adelete_for_runs([run_delete])
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run_keep in run_ids
async def test_delete_for_runs_removes_writes(
saver: BaseCheckpointSaver,
) -> None:
"""Associated writes cleaned up."""
tid = str(uuid4())
run1 = str(uuid4())
stored = await _put_with_run_id(saver, tid, run1)
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
# Pre-delete: verify writes exist
pre_tup = await saver.aget_tuple(stored)
assert pre_tup is not None, "Pre-delete: checkpoint should exist"
assert pre_tup.pending_writes is not None and len(pre_tup.pending_writes) == 1, (
f"Pre-delete: expected 1 write, got {len(pre_tup.pending_writes) if pre_tup.pending_writes else 0}"
)
await saver.adelete_for_runs([run1])
# The checkpoint (and its writes) should be gone
tup = await saver.aget_tuple(stored)
assert tup is None
async def test_delete_for_runs_empty_list_noop(
saver: BaseCheckpointSaver,
) -> None:
"""Empty list no error."""
await saver.adelete_for_runs([])
async def test_delete_for_runs_nonexistent_noop(
saver: BaseCheckpointSaver,
) -> None:
"""Missing run_ids no error."""
await saver.adelete_for_runs([str(uuid4())])
async def test_delete_for_runs_across_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""All namespaces cleaned."""
tid = str(uuid4())
run1 = str(uuid4())
await _put_with_run_id(saver, tid, run1, checkpoint_ns="")
await _put_with_run_id(saver, tid, run1, checkpoint_ns="child:1")
# Pre-delete: verify run1 present in both namespaces
for ns in ["", "child:1"]:
pre_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
pre_results.append(tup)
pre_run_ids = {t.metadata.get("run_id") for t in pre_results}
assert run1 in pre_run_ids, f"Pre-delete: run1 should exist in ns='{ns}'"
await saver.adelete_for_runs([run1])
for ns in ["", "child:1"]:
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
results.append(tup)
run_ids = {t.metadata.get("run_id") for t in results}
assert run1 not in run_ids
ALL_DELETE_FOR_RUNS_TESTS = [
test_delete_for_runs_single,
test_delete_for_runs_multiple,
test_delete_for_runs_preserves_other_runs,
test_delete_for_runs_removes_writes,
test_delete_for_runs_empty_list_noop,
test_delete_for_runs_nonexistent_noop,
test_delete_for_runs_across_namespaces,
]
async def run_delete_for_runs_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all delete_for_runs tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_DELETE_FOR_RUNS_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("delete_for_runs", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"delete_for_runs", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,149 +0,0 @@
"""DELETE_THREAD capability tests — adelete_thread."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_delete_thread_removes_checkpoints(
saver: BaseCheckpointSaver,
) -> None:
"""All checkpoints gone after delete."""
tid = str(uuid4())
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
# Pre-delete: verify data exists
assert await saver.aget_tuple(generate_config(tid)) is not None, (
"Pre-delete: checkpoint should exist"
)
await saver.adelete_thread(tid)
tup = await saver.aget_tuple(generate_config(tid))
assert tup is None
results = []
async for t in saver.alist(generate_config(tid)):
results.append(t)
assert len(results) == 0
async def test_delete_thread_removes_writes(saver: BaseCheckpointSaver) -> None:
"""Pending writes gone after delete."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
# Pre-delete: verify writes exist
pre_tup = await saver.aget_tuple(generate_config(tid))
assert pre_tup is not None, "Pre-delete: checkpoint should exist"
assert pre_tup.pending_writes is not None and len(pre_tup.pending_writes) == 1, (
f"Pre-delete: expected 1 write, got {len(pre_tup.pending_writes) if pre_tup.pending_writes else 0}"
)
await saver.adelete_thread(tid)
tup = await saver.aget_tuple(generate_config(tid))
assert tup is None
async def test_delete_thread_removes_all_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Root + child namespaces both removed."""
tid = str(uuid4())
for ns in ["", "child:1"]:
cfg = generate_config(tid, checkpoint_ns=ns)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
# Pre-delete: verify each namespace has data
for ns in ["", "child:1"]:
pre = await saver.aget_tuple(generate_config(tid, checkpoint_ns=ns))
assert pre is not None, f"Pre-delete: namespace '{ns}' should have data"
await saver.adelete_thread(tid)
for ns in ["", "child:1"]:
tup = await saver.aget_tuple(generate_config(tid, checkpoint_ns=ns))
assert tup is None
async def test_delete_thread_preserves_other_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Other threads untouched."""
tid1, tid2 = str(uuid4()), str(uuid4())
for tid in (tid1, tid2):
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
await saver.adelete_thread(tid1)
assert await saver.aget_tuple(generate_config(tid1)) is None
assert await saver.aget_tuple(generate_config(tid2)) is not None
async def test_delete_thread_nonexistent_noop(
saver: BaseCheckpointSaver,
) -> None:
"""No error for missing thread."""
# Should not raise
await saver.adelete_thread(str(uuid4()))
ALL_DELETE_THREAD_TESTS = [
test_delete_thread_removes_checkpoints,
test_delete_thread_removes_writes,
test_delete_thread_removes_all_namespaces,
test_delete_thread_preserves_other_threads,
test_delete_thread_nonexistent_noop,
]
async def run_delete_thread_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all delete_thread tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_DELETE_THREAD_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("delete_thread", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"delete_thread", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,253 +0,0 @@
"""GET_TUPLE capability tests — aget_tuple retrieval."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_get_tuple_nonexistent_returns_none(
saver: BaseCheckpointSaver,
) -> None:
"""Missing thread returns None."""
config = generate_config(str(uuid4()))
tup = await saver.aget_tuple(config)
assert tup is None
async def test_get_tuple_latest_when_no_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Returns newest checkpoint when no checkpoint_id in config."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
ids.append(cp["id"])
# Get without checkpoint_id — should return the latest
tup = await saver.aget_tuple(generate_config(tid))
assert tup is not None
assert tup.checkpoint["id"] == ids[-1]
assert tup.metadata["step"] == 2, (
f"Expected latest step=2, got {tup.metadata['step']}"
)
async def test_get_tuple_specific_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Returns exact match when checkpoint_id specified."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
await saver.aput(config2, cp2, generate_metadata(step=1), {})
# Fetch the first one specifically
tup = await saver.aget_tuple(stored1)
assert tup is not None
assert tup.checkpoint["id"] == cp1["id"]
async def test_get_tuple_config_structure(saver: BaseCheckpointSaver) -> None:
"""tuple.config has thread_id, checkpoint_ns, checkpoint_id."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
tup = await saver.aget_tuple(stored)
assert tup is not None
conf = tup.config["configurable"]
assert conf["thread_id"] == tid
assert conf.get("checkpoint_ns", "") == "", (
f"Expected checkpoint_ns='', got {conf.get('checkpoint_ns')!r}"
)
assert conf["checkpoint_id"] == cp["id"]
async def test_get_tuple_checkpoint_fields(saver: BaseCheckpointSaver) -> None:
"""All Checkpoint fields present."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint(channel_values={"k": "v"})
cp["channel_versions"] = {"k": 1}
stored = await saver.aput(config, cp, generate_metadata(), {"k": 1})
tup = await saver.aget_tuple(stored)
assert tup is not None
c = tup.checkpoint
assert c["id"] == cp["id"], f"id mismatch: {c['id']!r} != {cp['id']!r}"
assert c["v"] == 1, f"Expected v=1, got {c['v']!r}"
assert "ts" in c and c["ts"], "ts should be non-empty"
assert c["channel_values"] == {"k": "v"}, f"channel_values: {c['channel_values']!r}"
assert "channel_versions" in c
assert "versions_seen" in c
async def test_get_tuple_metadata(saver: BaseCheckpointSaver) -> None:
"""metadata populated correctly."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
md = generate_metadata(source="input", step=-1)
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata["source"] == "input"
assert tup.metadata["step"] == -1
async def test_get_tuple_parent_config(saver: BaseCheckpointSaver) -> None:
"""parent_config when parent exists, None otherwise."""
tid = str(uuid4())
# First checkpoint — no parent
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
tup1 = await saver.aget_tuple(stored1)
assert tup1 is not None
assert tup1.parent_config is None
# Second checkpoint — has parent
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.parent_config is not None
assert (
tup2.parent_config["configurable"]["checkpoint_id"]
== stored1["configurable"]["checkpoint_id"]
)
async def test_get_tuple_pending_writes(saver: BaseCheckpointSaver) -> None:
"""pending_writes from put_writes visible."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][0] == task_id, (
f"task_id mismatch: {tup.pending_writes[0][0]!r}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_get_tuple_respects_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns filtering."""
tid = str(uuid4())
cfg_root = generate_config(tid, checkpoint_ns="")
cp_root = generate_checkpoint()
stored_root = await saver.aput(cfg_root, cp_root, generate_metadata(), {})
cfg_child = generate_config(tid, checkpoint_ns="child:1")
cp_child = generate_checkpoint()
stored_child = await saver.aput(cfg_child, cp_child, generate_metadata(), {})
tup_root = await saver.aget_tuple(stored_root)
assert tup_root is not None
assert tup_root.checkpoint["id"] == cp_root["id"]
tup_child = await saver.aget_tuple(stored_child)
assert tup_child is not None
assert tup_child.checkpoint["id"] == cp_child["id"]
async def test_get_tuple_nonexistent_checkpoint_id(
saver: BaseCheckpointSaver,
) -> None:
"""Specific but missing checkpoint_id returns None."""
tid = str(uuid4())
nonexistent_id = str(uuid4())
# Put one checkpoint so the thread exists
config = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(config, cp, generate_metadata(), {})
# Ask for a non-existent checkpoint_id
bad_cfg = generate_config(tid, checkpoint_id=nonexistent_id)
tup = await saver.aget_tuple(bad_cfg)
assert tup is None
ALL_GET_TUPLE_TESTS = [
test_get_tuple_nonexistent_returns_none,
test_get_tuple_latest_when_no_checkpoint_id,
test_get_tuple_specific_checkpoint_id,
test_get_tuple_config_structure,
test_get_tuple_checkpoint_fields,
test_get_tuple_metadata,
test_get_tuple_parent_config,
test_get_tuple_pending_writes,
test_get_tuple_respects_namespace,
test_get_tuple_nonexistent_checkpoint_id,
]
async def run_get_tuple_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all get_tuple tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_GET_TUPLE_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("get_tuple", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"get_tuple", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,414 +0,0 @@
"""LIST capability tests — alist with various filters."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_list_data(saver: BaseCheckpointSaver) -> dict:
"""Populate saver with test data for list tests. Returns lookup info."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(4):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
source = "input" if i % 2 == 0 else "loop"
md = generate_metadata(source=source, step=i)
parent_cfg = await saver.aput(config, cp, md, {})
ids.append(cp["id"])
return {
"thread_id": tid,
"checkpoint_ids": ids,
"latest_config": parent_cfg,
}
async def test_list_all(saver: BaseCheckpointSaver) -> None:
"""No filters returns all checkpoints for the thread."""
data = await _setup_list_data(saver)
tid = data["thread_id"]
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 4
async def test_list_by_thread(saver: BaseCheckpointSaver) -> None:
"""Filter by thread_id — other threads not returned."""
data = await _setup_list_data(saver)
# List for a non-existent thread
results = []
async for tup in saver.alist(generate_config(str(uuid4()))):
results.append(tup)
assert len(results) == 0
# List for actual thread
results = []
async for tup in saver.alist(generate_config(data["thread_id"])):
results.append(tup)
assert len(results) == 4
async def test_list_by_namespace(saver: BaseCheckpointSaver) -> None:
"""Filter by checkpoint_ns."""
tid = str(uuid4())
# Root namespace
cfg1 = generate_config(tid, checkpoint_ns="")
cp1 = generate_checkpoint()
await saver.aput(cfg1, cp1, generate_metadata(), {})
# Child namespace
cfg2 = generate_config(tid, checkpoint_ns="child:1")
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(), {})
root_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="")):
root_results.append(tup)
assert len(root_results) == 1
child_results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="child:1")):
child_results.append(tup)
assert len(child_results) == 1
async def test_list_ordering(saver: BaseCheckpointSaver) -> None:
"""Newest first (descending checkpoint_id)."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
results = []
async for tup in saver.alist(generate_config(data["thread_id"])):
results.append(tup.checkpoint["id"])
# Should be in reverse order (newest first)
assert results == list(reversed(ids))
async def test_list_metadata_filter_single_key(
saver: BaseCheckpointSaver,
) -> None:
"""filter={'source': 'input'} returns only input checkpoints."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "input"},
):
results.append(tup)
assert len(results) == 2, (
f"Expected 2 'input' checkpoints (steps 0,2), got {len(results)}"
)
for tup in results:
assert tup.metadata["source"] == "input"
async def test_list_metadata_filter_step(saver: BaseCheckpointSaver) -> None:
"""filter={'step': 1} returns matching checkpoints."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"step": 1},
):
results.append(tup)
assert len(results) == 1
assert results[0].metadata["step"] == 1
async def test_list_before(saver: BaseCheckpointSaver) -> None:
"""Pagination cursor — only checkpoints before the given one."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
# Use the 3rd checkpoint as the 'before' cursor (index 2)
before_cfg = generate_config(data["thread_id"], checkpoint_id=ids[2])
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
before=before_cfg,
):
results.append(tup)
# Should only include checkpoints before ids[2]
result_ids = [t.checkpoint["id"] for t in results]
assert ids[2] not in result_ids
assert ids[3] not in result_ids
assert set(result_ids) == {ids[0], ids[1]}, (
f"Expected {{ids[0], ids[1]}}, got {set(result_ids)}"
)
async def test_list_limit(saver: BaseCheckpointSaver) -> None:
"""limit=1, limit=N."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(generate_config(data["thread_id"]), limit=1):
results.append(tup)
assert len(results) == 1
results = []
async for tup in saver.alist(generate_config(data["thread_id"]), limit=2):
results.append(tup)
assert len(results) == 2
async def test_list_limit_plus_before(saver: BaseCheckpointSaver) -> None:
"""Pagination with limit."""
data = await _setup_list_data(saver)
ids = data["checkpoint_ids"]
before_cfg = generate_config(data["thread_id"], checkpoint_id=ids[3])
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
before=before_cfg,
limit=1,
):
results.append(tup)
assert len(results) == 1
assert results[0].checkpoint["id"] == ids[2]
async def test_list_combined_thread_and_filter(
saver: BaseCheckpointSaver,
) -> None:
"""thread_id + metadata filter combined."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "loop"},
):
results.append(tup)
assert len(results) == 2, (
f"Expected 2 'loop' checkpoints (steps 1,3), got {len(results)}"
)
for tup in results:
assert tup.metadata["source"] == "loop"
async def test_list_empty_result(saver: BaseCheckpointSaver) -> None:
"""No matches returns empty."""
results = []
async for tup in saver.alist(
generate_config(str(uuid4())),
filter={"source": "nonexistent"},
):
results.append(tup)
assert len(results) == 0
async def test_list_includes_pending_writes(saver: BaseCheckpointSaver) -> None:
"""pending_writes in listed tuples."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
await saver.aput_writes(stored, [("ch", "val")], str(uuid4()))
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert results[0].pending_writes is not None
assert len(results[0].pending_writes) == 1, (
f"Expected 1 write, got {len(results[0].pending_writes)}"
)
assert results[0].pending_writes[0][1] == "ch", (
f"channel mismatch: {results[0].pending_writes[0][1]!r}"
)
assert results[0].pending_writes[0][2] == "val", (
f"value mismatch: {results[0].pending_writes[0][2]!r}"
)
async def test_list_multiple_namespaces(saver: BaseCheckpointSaver) -> None:
"""Root namespace checkpoint listed correctly."""
tid = str(uuid4())
for ns in ["", "child:1", "child:2"]:
cfg = generate_config(tid, checkpoint_ns=ns)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(), {})
# List with root namespace filter — should return exactly the root checkpoint
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns="")):
results.append(tup)
assert len(results) == 1, f"Expected 1 root checkpoint, got {len(results)}"
async def test_list_metadata_filter_multiple_keys(
saver: BaseCheckpointSaver,
) -> None:
"""filter with multiple keys — all must match."""
tid = str(uuid4())
# Create checkpoints with different metadata combos
for source, step in [("input", 1), ("loop", 1), ("input", 2)]:
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(source=source, step=step), {})
results = []
async for tup in saver.alist(
generate_config(tid),
filter={"source": "input", "step": 2},
):
results.append(tup)
assert len(results) == 1, (
f"Expected 1 match for source=input+step=2, got {len(results)}"
)
assert results[0].metadata["source"] == "input"
assert results[0].metadata["step"] == 2
async def test_list_metadata_filter_no_match(
saver: BaseCheckpointSaver,
) -> None:
"""Multi-key filter that matches nothing returns empty."""
data = await _setup_list_data(saver)
results = []
async for tup in saver.alist(
generate_config(data["thread_id"]),
filter={"source": "update", "step": 99},
):
results.append(tup)
assert len(results) == 0
async def test_list_metadata_custom_keys(
saver: BaseCheckpointSaver,
) -> None:
"""Custom (non-standard) metadata keys are filterable."""
tid = str(uuid4())
cfg = generate_config(tid)
cp = generate_checkpoint()
await saver.aput(cfg, cp, generate_metadata(score=42, run_id="run-abc"), {})
cfg2 = generate_config(tid)
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(score=99, run_id="run-xyz"), {})
# Filter by custom key
results = []
async for tup in saver.alist(
generate_config(tid),
filter={"score": 42},
):
results.append(tup)
assert len(results) == 1
assert results[0].metadata["score"] == 42
assert results[0].metadata["run_id"] == "run-abc"
async def test_list_global_search(
saver: BaseCheckpointSaver,
) -> None:
"""alist(None, filter=...) searches across all threads."""
tid1, tid2 = str(uuid4()), str(uuid4())
# Use a unique marker so we don't collide with other tests' data
marker = str(uuid4())
cfg1 = generate_config(tid1)
cp1 = generate_checkpoint()
await saver.aput(cfg1, cp1, generate_metadata(source="input", marker=marker), {})
cfg2 = generate_config(tid2)
cp2 = generate_checkpoint()
await saver.aput(cfg2, cp2, generate_metadata(source="loop", marker=marker), {})
# Search across all threads with filter
results = []
async for tup in saver.alist(None, filter={"source": "input", "marker": marker}):
results.append(tup)
assert len(results) == 1
assert results[0].config["configurable"]["thread_id"] == tid1
# Search with marker only — should find both
results = []
async for tup in saver.alist(None, filter={"marker": marker}):
results.append(tup)
assert len(results) == 2
ALL_LIST_TESTS = [
test_list_all,
test_list_by_thread,
test_list_by_namespace,
test_list_ordering,
test_list_metadata_filter_single_key,
test_list_metadata_filter_step,
test_list_metadata_filter_multiple_keys,
test_list_metadata_filter_no_match,
test_list_metadata_custom_keys,
test_list_global_search,
test_list_before,
test_list_limit,
test_list_limit_plus_before,
test_list_combined_thread_and_filter,
test_list_empty_result,
test_list_includes_pending_writes,
test_list_multiple_namespaces,
]
async def run_list_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all list tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_LIST_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("list", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("list", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,217 +0,0 @@
"""PRUNE capability tests — aprune(strategy)."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def _setup_thread(saver: BaseCheckpointSaver, tid: str, n: int = 3) -> list[dict]:
"""Create n checkpoints on tid. Returns stored configs."""
stored = []
parent_cfg = None
for i in range(n):
config = generate_config(tid)
if parent_cfg:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent_cfg = await saver.aput(config, cp, generate_metadata(step=i), {})
stored.append(parent_cfg)
return stored
async def test_prune_keep_latest_single_thread(
saver: BaseCheckpointSaver,
) -> None:
"""Only latest checkpoint survives."""
tid = str(uuid4())
configs = await _setup_thread(saver, tid, n=4)
await saver.aprune([tid], strategy="keep_latest")
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== configs[-1]["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_multiple_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Each thread keeps its latest."""
tid1, tid2 = str(uuid4()), str(uuid4())
c1 = await _setup_thread(saver, tid1, n=3)
c2 = await _setup_thread(saver, tid2, n=2)
await saver.aprune([tid1, tid2], strategy="keep_latest")
for tid, expected_last in [(tid1, c1[-1]), (tid2, c2[-1])]:
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== expected_last["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_across_namespaces(
saver: BaseCheckpointSaver,
) -> None:
"""Latest per namespace kept."""
tid = str(uuid4())
# Root namespace: 3 checkpoints
parent = None
for i in range(3):
cfg = generate_config(tid, checkpoint_ns="")
if parent:
cfg["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent = await saver.aput(cfg, cp, generate_metadata(step=i), {})
root_latest = parent
# Child namespace: 2 checkpoints
parent = None
for i in range(2):
cfg = generate_config(tid, checkpoint_ns="child:1")
if parent:
cfg["configurable"]["checkpoint_id"] = parent["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
parent = await saver.aput(cfg, cp, generate_metadata(step=i), {})
child_latest = parent
assert root_latest is not None
assert child_latest is not None
await saver.aprune([tid], strategy="keep_latest")
for ns, expected in [("", root_latest), ("child:1", child_latest)]:
results = []
async for tup in saver.alist(generate_config(tid, checkpoint_ns=ns)):
results.append(tup)
assert len(results) == 1
assert (
results[0].config["configurable"]["checkpoint_id"]
== expected["configurable"]["checkpoint_id"]
)
async def test_prune_keep_latest_preserves_writes(
saver: BaseCheckpointSaver,
) -> None:
"""Latest checkpoint's writes kept."""
tid = str(uuid4())
configs = await _setup_thread(saver, tid, n=3)
# Add writes to the latest
await saver.aput_writes(configs[-1], [("ch", "val")], str(uuid4()))
await saver.aprune([tid], strategy="keep_latest")
tup = await saver.aget_tuple(generate_config(tid))
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected 1 write, got {len(tup.pending_writes)}"
)
assert tup.pending_writes[0][1] == "ch", (
f"channel mismatch: {tup.pending_writes[0][1]!r}"
)
assert tup.pending_writes[0][2] == "val", (
f"value mismatch: {tup.pending_writes[0][2]!r}"
)
async def test_prune_delete_all(saver: BaseCheckpointSaver) -> None:
"""delete_all strategy removes everything."""
tid = str(uuid4())
await _setup_thread(saver, tid, n=3)
await saver.aprune([tid], strategy="delete")
results = []
async for tup in saver.alist(generate_config(tid)):
results.append(tup)
assert len(results) == 0
async def test_prune_preserves_other_threads(
saver: BaseCheckpointSaver,
) -> None:
"""Unlisted threads untouched."""
tid1, tid2 = str(uuid4()), str(uuid4())
await _setup_thread(saver, tid1, n=3)
await _setup_thread(saver, tid2, n=2)
# Snapshot tid2 before prune
pre_ids = []
async for tup in saver.alist(generate_config(tid2)):
pre_ids.append(tup.checkpoint["id"])
await saver.aprune([tid1], strategy="keep_latest")
# tid2 should be fully intact — same checkpoint IDs
post_ids = []
async for tup in saver.alist(generate_config(tid2)):
post_ids.append(tup.checkpoint["id"])
assert post_ids == pre_ids, f"tid2 changed: {pre_ids} -> {post_ids}"
async def test_prune_empty_list_noop(saver: BaseCheckpointSaver) -> None:
"""Empty thread_ids no error."""
await saver.aprune([], strategy="keep_latest")
async def test_prune_nonexistent_noop(saver: BaseCheckpointSaver) -> None:
"""Missing threads no error."""
await saver.aprune([str(uuid4())], strategy="keep_latest")
ALL_PRUNE_TESTS = [
test_prune_keep_latest_single_thread,
test_prune_keep_latest_multiple_threads,
test_prune_keep_latest_across_namespaces,
test_prune_keep_latest_preserves_writes,
test_prune_delete_all,
test_prune_preserves_other_threads,
test_prune_empty_list_noop,
test_prune_nonexistent_noop,
]
async def run_prune_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all prune tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PRUNE_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("prune", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("prune", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,411 +0,0 @@
"""PUT capability tests — aput + aget_tuple round-trip."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from typing import Any
from uuid import uuid4
from langgraph.checkpoint.base import (
BaseCheckpointSaver,
ChannelVersions,
)
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_put_returns_config(saver: BaseCheckpointSaver) -> None:
"""aput returns a RunnableConfig with thread_id, checkpoint_ns, checkpoint_id."""
config = generate_config()
cp = generate_checkpoint(channel_values={"k": "v"})
cp["channel_versions"] = {"k": 1}
md = generate_metadata()
result = await saver.aput(config, cp, md, {"k": 1})
assert "configurable" in result
conf = result["configurable"]
assert "thread_id" in conf
assert "checkpoint_ns" in conf
assert "checkpoint_id" in conf
assert conf["checkpoint_id"] == cp["id"]
async def test_put_roundtrip(saver: BaseCheckpointSaver) -> None:
"""put then get_tuple returns identical checkpoint."""
config = generate_config()
cp = generate_checkpoint(channel_values={"msg": "hello"})
cp["channel_versions"] = {"msg": 1}
md = generate_metadata(source="input", step=-1)
stored_config = await saver.aput(config, cp, md, {"msg": 1})
tup = await saver.aget_tuple(stored_config)
assert tup is not None
assert tup.checkpoint["id"] == cp["id"]
assert tup.checkpoint["channel_values"] == {"msg": "hello"}
async def test_put_preserves_channel_values(saver: BaseCheckpointSaver) -> None:
"""Various types (str, int, list, dict, bytes, None) round-trip correctly."""
values: dict[str, Any] = {
"str_val": "hello",
"int_val": 42,
"list_val": [1, 2, 3],
"dict_val": {"nested": True},
}
config = generate_config()
cp = generate_checkpoint(channel_values=values)
versions: ChannelVersions = {k: 1 for k in values}
cp["channel_versions"] = versions
md = generate_metadata()
stored = await saver.aput(config, cp, md, versions)
tup = await saver.aget_tuple(stored)
assert tup is not None
for k, v in values.items():
assert tup.checkpoint["channel_values"].get(k) == v, (
f"channel_values[{k}]: expected {v!r}, got {tup.checkpoint['channel_values'].get(k)!r}"
)
async def test_put_preserves_channel_versions(saver: BaseCheckpointSaver) -> None:
"""ChannelVersions round-trip correctly."""
versions: ChannelVersions = {"a": 1, "b": 2}
config = generate_config()
cp = generate_checkpoint(
channel_values={"a": "x", "b": "y"}, channel_versions=versions
)
md = generate_metadata()
stored = await saver.aput(config, cp, md, versions)
tup = await saver.aget_tuple(stored)
assert tup is not None
# Compare version values — checkpointers may convert int to str
for k, expected in versions.items():
actual = tup.checkpoint["channel_versions"].get(k)
assert actual is not None, f"channel_versions[{k}] missing"
assert str(actual).split(".")[0] == str(expected).split(".")[0], (
f"channel_versions[{k}]: expected {expected!r}, got {actual!r}"
)
async def test_put_preserves_versions_seen(saver: BaseCheckpointSaver) -> None:
"""versions_seen dict round-trips."""
vs: dict[str, ChannelVersions] = {"node1": {"ch": 1}, "node2": {"ch": 2}}
config = generate_config()
cp = generate_checkpoint(versions_seen=vs)
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
for node in vs:
assert node in tup.checkpoint["versions_seen"], f"versions_seen[{node}] missing"
async def test_put_preserves_metadata(saver: BaseCheckpointSaver) -> None:
"""Metadata source, step, parents, and custom keys round-trip."""
md = generate_metadata(source="loop", step=3, custom_key="custom_value")
config = generate_config()
cp = generate_checkpoint()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata["source"] == "loop"
assert tup.metadata["step"] == 3
assert tup.metadata.get("custom_key") == "custom_value"
async def test_put_root_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns='' works."""
config = generate_config(checkpoint_ns="")
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.config["configurable"].get("checkpoint_ns", "") == ""
async def test_put_child_namespace(saver: BaseCheckpointSaver) -> None:
"""checkpoint_ns='child:abc' works."""
config = generate_config(checkpoint_ns="child:abc")
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.config["configurable"]["checkpoint_ns"] == "child:abc"
async def test_put_default_namespace(saver: BaseCheckpointSaver) -> None:
"""Config without checkpoint_ns defaults to ''."""
tid = str(uuid4())
config = {"configurable": {"thread_id": tid, "checkpoint_ns": ""}}
cp = generate_checkpoint()
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
async def test_put_multiple_checkpoints_same_thread(
saver: BaseCheckpointSaver,
) -> None:
"""Sequential puts on same thread, all retrievable."""
tid = str(uuid4())
ids = []
parent_cfg = None
for i in range(3):
config = generate_config(tid)
if parent_cfg is not None:
config["configurable"]["checkpoint_id"] = parent_cfg["configurable"][
"checkpoint_id"
]
cp = generate_checkpoint()
md = generate_metadata(step=i)
parent_cfg = await saver.aput(config, cp, md, {})
ids.append(cp["id"])
# All three should be retrievable
for cid in ids:
cfg = generate_config(tid, checkpoint_id=cid)
tup = await saver.aget_tuple(cfg)
assert tup is not None, f"checkpoint {cid} not found"
assert tup.checkpoint["id"] == cid
async def test_put_multiple_threads_isolated(saver: BaseCheckpointSaver) -> None:
"""Different thread_ids don't interfere."""
tid1, tid2 = str(uuid4()), str(uuid4())
config1 = generate_config(tid1)
cp1 = generate_checkpoint(channel_values={"x": "thread1"})
cp1["channel_versions"] = {"x": 1}
await saver.aput(config1, cp1, generate_metadata(), {"x": 1})
config2 = generate_config(tid2)
cp2 = generate_checkpoint(channel_values={"x": "thread2"})
cp2["channel_versions"] = {"x": 1}
await saver.aput(config2, cp2, generate_metadata(), {"x": 1})
tup1 = await saver.aget_tuple(generate_config(tid1))
tup2 = await saver.aget_tuple(generate_config(tid2))
assert tup1 is not None and tup2 is not None
assert tup1.checkpoint["channel_values"]["x"] == "thread1"
assert tup2.checkpoint["channel_values"]["x"] == "thread2"
async def test_put_parent_config(saver: BaseCheckpointSaver) -> None:
"""parent checkpoint_id tracked correctly."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {})
# Second checkpoint — its config carries the parent checkpoint_id
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup = await saver.aget_tuple(stored2)
assert tup is not None
assert tup.parent_config is not None
assert (
tup.parent_config["configurable"]["checkpoint_id"]
== stored1["configurable"]["checkpoint_id"]
)
async def test_put_incremental_channel_update(saver: BaseCheckpointSaver) -> None:
"""Only updated channels need new blobs; unchanged channels loaded from prior versions."""
tid = str(uuid4())
# Checkpoint 1: both channels are new
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1", "b": "v2"},
channel_versions={"a": 1, "b": 1},
)
stored1 = await saver.aput(
config1, cp1, generate_metadata(step=0), {"a": 1, "b": 1}
)
# Checkpoint 2: only 'a' is updated
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1_updated", "b": "v2"},
channel_versions={"a": 2, "b": 1},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"a": 2})
# cp2 should reconstruct full channel_values from blobs at mixed versions
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1_updated", (
f"a: expected 'v1_updated', got {tup2.checkpoint['channel_values'].get('a')!r}"
)
assert tup2.checkpoint["channel_values"].get("b") == "v2", (
f"b: expected 'v2', got {tup2.checkpoint['channel_values'].get('b')!r}"
)
# cp1 should still return original values
tup1 = await saver.aget_tuple(stored1)
assert tup1 is not None
assert tup1.checkpoint["channel_values"].get("a") == "v1"
assert tup1.checkpoint["channel_values"].get("b") == "v2"
async def test_put_new_channel_added(saver: BaseCheckpointSaver) -> None:
"""A channel that appears for the first time in a later checkpoint."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1"},
channel_versions={"a": 1},
)
stored1 = await saver.aput(config1, cp1, generate_metadata(step=0), {"a": 1})
# Checkpoint 2: 'b' is brand new, 'a' is unchanged
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1", "b": "new_channel"},
channel_versions={"a": 1, "b": 1},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"b": 1})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1", (
f"a: expected 'v1', got {tup2.checkpoint['channel_values'].get('a')!r}"
)
assert tup2.checkpoint["channel_values"].get("b") == "new_channel", (
f"b: expected 'new_channel', got {tup2.checkpoint['channel_values'].get('b')!r}"
)
async def test_put_channel_removed(saver: BaseCheckpointSaver) -> None:
"""Channel no longer in channel_versions should not appear in loaded values."""
tid = str(uuid4())
config1 = generate_config(tid)
cp1 = generate_checkpoint(
channel_values={"a": "v1", "b": "v2"},
channel_versions={"a": 1, "b": 1},
)
stored1 = await saver.aput(
config1, cp1, generate_metadata(step=0), {"a": 1, "b": 1}
)
# Checkpoint 2: 'b' dropped from channel_versions
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint(
channel_values={"a": "v1_updated"},
channel_versions={"a": 2},
)
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {"a": 2})
tup2 = await saver.aget_tuple(stored2)
assert tup2 is not None
assert tup2.checkpoint["channel_values"].get("a") == "v1_updated"
assert "b" not in tup2.checkpoint["channel_values"], (
f"'b' should not be present, got {tup2.checkpoint['channel_values']}"
)
async def test_put_preserves_run_id(saver: BaseCheckpointSaver) -> None:
"""run_id in metadata round-trips correctly."""
run_id = str(uuid4())
config = generate_config()
cp = generate_checkpoint()
md = generate_metadata(source="loop", step=0, run_id=run_id)
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.metadata.get("run_id") == run_id, (
f"run_id: expected {run_id!r}, got {tup.metadata.get('run_id')!r}"
)
async def test_put_preserves_versions_seen_values(saver: BaseCheckpointSaver) -> None:
"""versions_seen values (not just keys) round-trip correctly."""
vs: dict[str, ChannelVersions] = {
"node1": {"ch_a": 1, "ch_b": 2},
"node2": {"ch_a": 3},
}
config = generate_config()
cp = generate_checkpoint(versions_seen=vs)
md = generate_metadata()
stored = await saver.aput(config, cp, md, {})
tup = await saver.aget_tuple(stored)
assert tup is not None
for node, expected_versions in vs.items():
assert node in tup.checkpoint["versions_seen"], f"versions_seen[{node}] missing"
actual_versions = tup.checkpoint["versions_seen"][node]
for ch, expected_v in expected_versions.items():
actual_v = actual_versions.get(ch)
assert actual_v is not None, f"versions_seen[{node}][{ch}] missing"
assert str(actual_v).split(".")[0] == str(expected_v).split(".")[0], (
f"versions_seen[{node}][{ch}]: expected {expected_v!r}, got {actual_v!r}"
)
ALL_PUT_TESTS = [
test_put_returns_config,
test_put_roundtrip,
test_put_preserves_channel_values,
test_put_preserves_channel_versions,
test_put_preserves_versions_seen,
test_put_preserves_metadata,
test_put_root_namespace,
test_put_child_namespace,
test_put_default_namespace,
test_put_multiple_checkpoints_same_thread,
test_put_multiple_threads_isolated,
test_put_parent_config,
test_put_incremental_channel_update,
test_put_new_channel_added,
test_put_channel_removed,
test_put_preserves_run_id,
test_put_preserves_versions_seen_values,
]
async def run_put_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all put tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PUT_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("put", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result("put", test_fn.__name__, False, traceback.format_exc())
return passed, failed, failures
@@ -1,302 +0,0 @@
"""PUT_WRITES capability tests — aput_writes + pending_writes retrieval."""
from __future__ import annotations
import traceback
from collections.abc import Callable
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.test_utils import (
generate_checkpoint,
generate_config,
generate_metadata,
)
async def test_put_writes_basic(saver: BaseCheckpointSaver) -> None:
"""Write stored, visible in aget_tuple pending_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("channel1", "value1")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
# Verify exact write tuple: (task_id, channel, value)
matching = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "channel1"]
assert len(matching) == 1, f"Expected 1 write, got {len(matching)}"
assert matching[0][2] == "value1", f"Value mismatch: {matching[0][2]!r}"
async def test_put_writes_multiple_writes_same_task(
saver: BaseCheckpointSaver,
) -> None:
"""Multiple (channel, value) pairs in a single call."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
writes = [("ch1", "v1"), ("ch2", "v2"), ("ch3", "v3")]
await saver.aput_writes(stored, writes, task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 3, (
f"Expected 3 writes, got {len(tup.pending_writes)}"
)
channels = {w[1] for w in tup.pending_writes}
assert channels == {"ch1", "ch2", "ch3"}, f"Expected exact channels, got {channels}"
# Verify values per channel
for expected_ch, expected_val in writes:
match = [
w for w in tup.pending_writes if w[0] == task_id and w[1] == expected_ch
]
assert len(match) == 1, f"Expected 1 write for {expected_ch}, got {len(match)}"
assert match[0][2] == expected_val, (
f"Value mismatch for {expected_ch}: {match[0][2]!r}"
)
async def test_put_writes_multiple_tasks(saver: BaseCheckpointSaver) -> None:
"""Different task_ids produce separate writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
t1, t2 = str(uuid4()), str(uuid4())
await saver.aput_writes(stored, [("ch", "from_t1")], t1)
await saver.aput_writes(stored, [("ch", "from_t2")], t2)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 2, (
f"Expected 2 writes, got {len(tup.pending_writes)}"
)
# Verify values per task
t1_writes = [w for w in tup.pending_writes if w[0] == t1 and w[1] == "ch"]
t2_writes = [w for w in tup.pending_writes if w[0] == t2 and w[1] == "ch"]
assert len(t1_writes) == 1, f"Expected 1 write from t1, got {len(t1_writes)}"
assert len(t2_writes) == 1, f"Expected 1 write from t2, got {len(t2_writes)}"
assert t1_writes[0][2] == "from_t1", f"t1 value: {t1_writes[0][2]!r}"
assert t2_writes[0][2] == "from_t2", f"t2 value: {t2_writes[0][2]!r}"
async def test_put_writes_preserves_task_id(saver: BaseCheckpointSaver) -> None:
"""task_id in pending_writes matches what was passed to aput_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert any(w[0] == task_id for w in tup.pending_writes)
async def test_put_writes_preserves_channel_and_value(
saver: BaseCheckpointSaver,
) -> None:
"""Channel name + value round-trip."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("my_channel", {"data": 123})], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
match = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "my_channel"]
assert len(match) == 1, f"Expected 1 write, got {len(match)}"
assert match[0][2] == {"data": 123}, f"Value mismatch: {match[0][2]!r}"
async def test_put_writes_task_path(saver: BaseCheckpointSaver) -> None:
"""task_path parameter accepted without error."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
# Should not raise
await saver.aput_writes(stored, [("ch", "v")], task_id, task_path="a:b:c")
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1
async def test_put_writes_idempotent(saver: BaseCheckpointSaver) -> None:
"""Duplicate (task_id, idx) doesn't duplicate writes."""
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(stored, [("ch", "val")], task_id)
await saver.aput_writes(stored, [("ch", "val")], task_id)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
assert len(tup.pending_writes) == 1, (
f"Expected exactly 1 write total, got {len(tup.pending_writes)}"
)
# Should not have duplicated
matching = [w for w in tup.pending_writes if w[0] == task_id and w[1] == "ch"]
assert len(matching) == 1
async def test_put_writes_special_channels(saver: BaseCheckpointSaver) -> None:
"""ERROR and INTERRUPT channels handled correctly."""
from langgraph.checkpoint.serde.types import ERROR, INTERRUPT
tid = str(uuid4())
config = generate_config(tid)
cp = generate_checkpoint()
stored = await saver.aput(config, cp, generate_metadata(), {})
task_id = str(uuid4())
await saver.aput_writes(
stored,
[(ERROR, "something went wrong"), (INTERRUPT, {"reason": "human_input"})],
task_id,
)
tup = await saver.aget_tuple(stored)
assert tup is not None
assert tup.pending_writes is not None
channels = {w[1] for w in tup.pending_writes}
assert ERROR in channels
assert INTERRUPT in channels
# Verify values
err_writes = [w for w in tup.pending_writes if w[0] == task_id and w[1] == ERROR]
assert len(err_writes) == 1, f"Expected 1 ERROR write, got {len(err_writes)}"
assert err_writes[0][2] == "something went wrong", (
f"ERROR value: {err_writes[0][2]!r}"
)
int_writes = [
w for w in tup.pending_writes if w[0] == task_id and w[1] == INTERRUPT
]
assert len(int_writes) == 1, f"Expected 1 INTERRUPT write, got {len(int_writes)}"
assert int_writes[0][2] == {"reason": "human_input"}, (
f"INTERRUPT value: {int_writes[0][2]!r}"
)
async def test_put_writes_across_namespaces(saver: BaseCheckpointSaver) -> None:
"""Writes isolated by checkpoint_ns."""
tid = str(uuid4())
# Root namespace checkpoint + write
cfg_root = generate_config(tid, checkpoint_ns="")
cp_root = generate_checkpoint()
stored_root = await saver.aput(cfg_root, cp_root, generate_metadata(), {})
root_task = str(uuid4())
await saver.aput_writes(stored_root, [("ch", "root_val")], root_task)
# Child namespace checkpoint + write
cfg_child = generate_config(tid, checkpoint_ns="child:1")
cp_child = generate_checkpoint()
stored_child = await saver.aput(cfg_child, cp_child, generate_metadata(), {})
child_task = str(uuid4())
await saver.aput_writes(stored_child, [("ch", "child_val")], child_task)
# Verify isolation — root should have exactly 1 write with root_val
tup_root = await saver.aget_tuple(stored_root)
assert tup_root is not None
assert tup_root.pending_writes is not None
root_ch = [w for w in tup_root.pending_writes if w[1] == "ch"]
assert len(root_ch) == 1, f"Expected 1 root write, got {len(root_ch)}"
assert root_ch[0][2] == "root_val", f"Root value: {root_ch[0][2]!r}"
# Child should have exactly 1 write with child_val
tup_child = await saver.aget_tuple(stored_child)
assert tup_child is not None
assert tup_child.pending_writes is not None
child_ch = [w for w in tup_child.pending_writes if w[1] == "ch"]
assert len(child_ch) == 1, f"Expected 1 child write, got {len(child_ch)}"
assert child_ch[0][2] == "child_val", f"Child value: {child_ch[0][2]!r}"
async def test_put_writes_cleared_on_next_checkpoint(
saver: BaseCheckpointSaver,
) -> None:
"""New checkpoint starts with fresh pending_writes."""
tid = str(uuid4())
config = generate_config(tid)
cp1 = generate_checkpoint()
stored1 = await saver.aput(config, cp1, generate_metadata(step=0), {})
await saver.aput_writes(stored1, [("ch", "old_write")], str(uuid4()))
# New checkpoint
config2 = generate_config(tid)
config2["configurable"]["checkpoint_id"] = stored1["configurable"]["checkpoint_id"]
cp2 = generate_checkpoint()
stored2 = await saver.aput(config2, cp2, generate_metadata(step=1), {})
tup = await saver.aget_tuple(stored2)
assert tup is not None
# New checkpoint should have no pending writes
writes = tup.pending_writes or []
assert len(writes) == 0
ALL_PUT_WRITES_TESTS = [
test_put_writes_basic,
test_put_writes_multiple_writes_same_task,
test_put_writes_multiple_tasks,
test_put_writes_preserves_task_id,
test_put_writes_preserves_channel_and_value,
test_put_writes_task_path,
test_put_writes_idempotent,
test_put_writes_special_channels,
test_put_writes_across_namespaces,
test_put_writes_cleared_on_next_checkpoint,
]
async def run_put_writes_tests(
saver: BaseCheckpointSaver,
on_test_result: Callable[[str, str, bool, str | None], None] | None = None,
) -> tuple[int, int, list[str]]:
"""Run all put_writes tests. Returns (passed, failed, failure_names)."""
passed = 0
failed = 0
failures: list[str] = []
for test_fn in ALL_PUT_WRITES_TESTS:
try:
await test_fn(saver)
passed += 1
if on_test_result:
on_test_result("put_writes", test_fn.__name__, True, None)
except Exception as e:
failed += 1
msg = f"{test_fn.__name__}: {e}"
failures.append(msg)
if on_test_result:
on_test_result(
"put_writes", test_fn.__name__, False, traceback.format_exc()
)
return passed, failed, failures
@@ -1,212 +0,0 @@
"""Test utilities: checkpoint generators, assertion helpers, bulk operations."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
)
from langgraph.checkpoint.base.id import uuid6
def generate_checkpoint(
*,
checkpoint_id: str | None = None,
channel_values: dict[str, Any] | None = None,
channel_versions: ChannelVersions | None = None,
versions_seen: dict[str, ChannelVersions] | None = None,
) -> Checkpoint:
"""Create a well-formed Checkpoint with sensible defaults."""
return Checkpoint(
v=1,
id=checkpoint_id or str(uuid6(clock_seq=-1)),
ts=datetime.now(timezone.utc).isoformat(),
channel_values=channel_values if channel_values is not None else {},
channel_versions=channel_versions if channel_versions is not None else {},
versions_seen=versions_seen if versions_seen is not None else {},
pending_sends=[], # ty: ignore[invalid-key]
updated_channels=None,
)
def generate_config(
thread_id: str | None = None,
*,
checkpoint_ns: str = "",
checkpoint_id: str | None = None,
) -> RunnableConfig:
"""Create a RunnableConfig targeting a specific thread / namespace / checkpoint."""
configurable: dict[str, Any] = {
"thread_id": thread_id or str(uuid4()),
"checkpoint_ns": checkpoint_ns,
}
if checkpoint_id is not None:
configurable["checkpoint_id"] = checkpoint_id
return {"configurable": configurable}
def generate_metadata(
source: str = "loop",
step: int = 0,
**extra: Any,
) -> CheckpointMetadata:
"""Create CheckpointMetadata with defaults."""
md: dict[str, Any] = {"source": source, "step": step, "parents": {}}
md.update(extra)
return md
async def put_test_checkpoint(
saver: Any,
*,
thread_id: str | None = None,
checkpoint_ns: str = "",
parent_config: RunnableConfig | None = None,
channel_values: dict[str, Any] | None = None,
channel_versions: ChannelVersions | None = None,
metadata: CheckpointMetadata | None = None,
new_versions: ChannelVersions | None = None,
) -> RunnableConfig:
"""Put a single test checkpoint and return the stored config.
Handles wiring up parent_config, channel_values -> new_versions, etc.
"""
tid = thread_id or str(uuid4())
cp = generate_checkpoint(
channel_values=channel_values,
channel_versions=channel_versions,
)
# When channel_values are provided, ensure channel_versions + new_versions
# are consistent so the checkpointer stores the blobs correctly.
vals = channel_values or {}
cv = channel_versions
if cv is None and vals:
cv = {k: 1 for k in vals}
cp["channel_versions"] = cv
nv = new_versions
if nv is None:
nv = cv or {}
md = metadata or generate_metadata()
config = generate_config(tid, checkpoint_ns=checkpoint_ns)
if parent_config is not None:
config["configurable"]["checkpoint_id"] = parent_config["configurable"][
"checkpoint_id"
]
return await saver.aput(config, cp, md, nv)
async def put_test_checkpoints(
saver: Any,
*,
n_threads: int = 1,
n_checkpoints: int = 1,
namespaces: list[str] | None = None,
channel_values: dict[str, Any] | None = None,
) -> list[RunnableConfig]:
"""Convenience: put multiple checkpoints across threads/namespaces.
Returns the stored configs in insertion order.
"""
nss = namespaces or [""]
stored: list[RunnableConfig] = []
for t in range(n_threads):
tid = f"thread-{t}"
for ns in nss:
parent: RunnableConfig | None = None
for _c in range(n_checkpoints):
cfg = await put_test_checkpoint(
saver,
thread_id=tid,
checkpoint_ns=ns,
parent_config=parent,
channel_values=channel_values,
)
parent = cfg
stored.append(cfg)
return stored
def assert_checkpoint_equal(
actual: Checkpoint,
expected: Checkpoint,
*,
check_channel_values: bool = True,
) -> None:
"""Assert two checkpoints are semantically equal."""
assert actual["v"] == expected["v"], f"v mismatch: {actual['v']} != {expected['v']}"
assert actual["id"] == expected["id"], (
f"id mismatch: {actual['id']} != {expected['id']}"
)
assert actual["channel_versions"] == expected["channel_versions"], (
"channel_versions mismatch"
)
assert actual["versions_seen"] == expected["versions_seen"], (
"versions_seen mismatch"
)
if check_channel_values:
assert actual["channel_values"] == expected["channel_values"], (
"channel_values mismatch"
)
def assert_tuple_equal(
actual: CheckpointTuple,
expected: CheckpointTuple,
*,
check_writes: bool = True,
check_channel_values: bool = True,
) -> None:
"""Assert two CheckpointTuples are semantically equal."""
# Config
a_conf = actual.config["configurable"]
e_conf = expected.config["configurable"]
assert a_conf["thread_id"] == e_conf["thread_id"], (
f"thread_id mismatch: {a_conf['thread_id']} != {e_conf['thread_id']}"
)
assert a_conf.get("checkpoint_ns", "") == e_conf.get("checkpoint_ns", ""), (
"checkpoint_ns mismatch"
)
assert a_conf["checkpoint_id"] == e_conf["checkpoint_id"], "checkpoint_id mismatch"
# Checkpoint
assert_checkpoint_equal(
actual.checkpoint,
expected.checkpoint,
check_channel_values=check_channel_values,
)
# Metadata
for k, v in expected.metadata.items():
assert actual.metadata.get(k) == v, (
f"metadata[{k}] mismatch: {actual.metadata.get(k)} != {v}"
)
# Parent config
if expected.parent_config is not None:
assert actual.parent_config is not None, "expected parent_config, got None"
assert (
actual.parent_config["configurable"]["checkpoint_id"]
== expected.parent_config["configurable"]["checkpoint_id"]
), "parent checkpoint_id mismatch"
else:
assert actual.parent_config is None, (
f"expected no parent_config, got {actual.parent_config}"
)
# Pending writes
if check_writes and expected.pending_writes is not None:
assert actual.pending_writes is not None
assert len(actual.pending_writes) == len(expected.pending_writes), (
f"pending_writes length mismatch: {len(actual.pending_writes)} != {len(expected.pending_writes)}"
)

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