mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-13 21:27:52 +02:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f26ca07716 | ||
|
|
b250823532 | ||
|
|
120d34303d | ||
|
|
6ff9e4a764 | ||
|
|
3c36d2e2c8 | ||
|
|
f6d0382d66 | ||
|
|
0386fe5f6a | ||
|
|
b11ece823b | ||
|
|
46ce6ad927 | ||
|
|
0c929e62eb | ||
|
|
88c434048f | ||
|
|
f67a089a68 | ||
|
|
cc97fad7e5 | ||
|
|
75c73369a3 | ||
|
|
fdbcc07381 | ||
|
|
80e19ecf4d | ||
|
|
54272afe01 | ||
|
|
e1aeb24a4e | ||
|
|
a51c0bfa31 |
@@ -15,7 +15,7 @@ body:
|
|||||||
* [LangChain Forum](https://forum.langchain.com/),
|
* [LangChain Forum](https://forum.langchain.com/),
|
||||||
* [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
|
* [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
|
||||||
* [LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
|
* [LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
|
||||||
* [LangChain documentation with the integrated search](https://docs.langchain.com/),
|
* [LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
|
||||||
* [GitHub search](https://github.com/langchain-ai/langgraph),
|
* [GitHub search](https://github.com/langchain-ai/langgraph),
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checks
|
id: checks
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
version: 2.1
|
version: 2.1
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Documentation
|
|
||||||
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
|
|
||||||
about: Report an issue related to the LangGraph documentation
|
|
||||||
- name: LangChain Forum
|
- name: LangChain Forum
|
||||||
url: https://forum.langchain.com/
|
url: https://forum.langchain.com/
|
||||||
about: General community discussions and support
|
about: General community discussions, support, and feature requests
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Documentation
|
||||||
|
description: Report an issue related to the LangGraph documentation.
|
||||||
|
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||||
|
labels: [documentation]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Issue with current documentation:"
|
||||||
|
description: >
|
||||||
|
Please make sure to leave a reference to the document/code you're
|
||||||
|
referring to.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Idea or request for content:"
|
||||||
|
description: >
|
||||||
|
Please describe as clearly as possible what topics you think are missing
|
||||||
|
from the current documentation.
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import logging
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from urllib import error, request
|
from urllib import request, error
|
||||||
|
|
||||||
import langgraph_cli
|
import langgraph_cli
|
||||||
import langgraph_cli.config
|
import langgraph_cli.config
|
||||||
@@ -12,13 +11,9 @@ from langgraph_cli.constants import DEFAULT_PORT
|
|||||||
from langgraph_cli.exec import Runner, subp_exec
|
from langgraph_cli.exec import Runner, subp_exec
|
||||||
from langgraph_cli.progress import Progress
|
from langgraph_cli.progress import Progress
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
||||||
"""Spin up API with Postgres/Redis via docker compose and wait until ready."""
|
"""Spin up API with Postgres/Redis via docker compose and wait until ready."""
|
||||||
logger.info("Starting test...")
|
|
||||||
with Runner() as runner, Progress(message="Pulling...") as set:
|
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||||
# Detect docker/compose capabilities
|
# Detect docker/compose capabilities
|
||||||
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||||
@@ -62,9 +57,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
|||||||
sys.stderr.write(f"docker compose up failed: {e}\n")
|
sys.stderr.write(f"docker compose up failed: {e}\n")
|
||||||
try:
|
try:
|
||||||
sys.stderr.write("\n== docker compose ps ==\n")
|
sys.stderr.write("\n== docker compose ps ==\n")
|
||||||
runner.run(
|
runner.run(subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False))
|
||||||
subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False)
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@@ -100,7 +93,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
|||||||
set("")
|
set("")
|
||||||
base_url = f"http://localhost:{port}"
|
base_url = f"http://localhost:{port}"
|
||||||
ok_url = f"{base_url}/ok"
|
ok_url = f"{base_url}/ok"
|
||||||
logger.info(f"Waiting for {ok_url} to respond with 200...")
|
print(f"Waiting for {ok_url} to respond with 200...")
|
||||||
deadline = time.time() + 30
|
deadline = time.time() + 30
|
||||||
last_err: Exception | None = None
|
last_err: Exception | None = None
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -114,16 +107,13 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
last_err = RuntimeError(f"Unexpected status: {resp.status}")
|
last_err = RuntimeError(f"Unexpected status: {resp.status}")
|
||||||
logger.error(f"Unexpected status: {resp.status}")
|
print(f"Unexpected status: {resp.status}")
|
||||||
except error.URLError as e:
|
except error.URLError as e:
|
||||||
logger.error(f"URLError: {e}")
|
|
||||||
last_err = e
|
last_err = e
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error(f"Exception: {e}")
|
|
||||||
last_err = e
|
last_err = e
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
else:
|
else:
|
||||||
logger.error("Timeout waiting for /ok to return 200")
|
|
||||||
# Bring stack down before raising
|
# Bring stack down before raising
|
||||||
args_down = [*args, "down", "-v", "--remove-orphans"]
|
args_down = [*args, "down", "-v", "--remove-orphans"]
|
||||||
try:
|
try:
|
||||||
@@ -141,8 +131,6 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Clean up: bring compose stack down to free ports for next test
|
# Clean up: bring compose stack down to free ports for next test
|
||||||
logger.info("Test succeeded. Bringing down compose stack...")
|
|
||||||
try:
|
|
||||||
args_down = [*args, "down", "-v", "--remove-orphans"]
|
args_down = [*args, "down", "-v", "--remove-orphans"]
|
||||||
runner.run(
|
runner.run(
|
||||||
subp_exec(
|
subp_exec(
|
||||||
@@ -152,12 +140,6 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
|||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.info("Compose stack down. Finishing...")
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to bring down compose stack")
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.info("Test finished")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -168,10 +150,4 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
|
parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
|
||||||
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT)
|
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
|
||||||
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
|
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
|
||||||
except BaseException:
|
|
||||||
logger.exception("Test failed")
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.info("Test execution finished")
|
|
||||||
|
|||||||
@@ -13,26 +13,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.14"
|
- "3.11"
|
||||||
example:
|
|
||||||
- name: A
|
|
||||||
workdir: libs/cli/examples
|
|
||||||
tag: langgraph-test-a
|
|
||||||
- name: B
|
|
||||||
workdir: libs/cli/examples/graphs
|
|
||||||
tag: langgraph-test-b
|
|
||||||
- name: C
|
|
||||||
workdir: libs/cli/examples/graphs_reqs_a
|
|
||||||
tag: langgraph-test-c
|
|
||||||
- name: D
|
|
||||||
workdir: libs/cli/examples/graphs_reqs_b
|
|
||||||
tag: langgraph-test-d
|
|
||||||
name: "CLI integration test"
|
name: "CLI integration test"
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: libs/cli
|
working-directory: libs/cli
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: Ana06/get-changed-files@v2.3.0
|
uses: Ana06/get-changed-files@v2.3.0
|
||||||
@@ -40,79 +27,63 @@ jobs:
|
|||||||
filter: "libs/cli/**"
|
filter: "libs/cli/**"
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "cli-integration-test"
|
cache-suffix: "cli-integration-test"
|
||||||
ignore-nothing-to-cache: true
|
ignore-nothing-to-cache: true
|
||||||
|
- name: Setup env
|
||||||
|
if: steps.changed-files.outputs.all
|
||||||
|
working-directory: libs/cli/examples
|
||||||
|
run: cat .env.example > .env
|
||||||
- name: Install cli globally
|
- name: Install cli globally
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
run: pip install -e .
|
run: pip install -e .
|
||||||
- name: Build and test service ${{ matrix.example.name }}
|
- name: Build and test service A
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
working-directory: ${{ matrix.example.workdir }}
|
working-directory: libs/cli/examples
|
||||||
env:
|
env:
|
||||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
# Build the image for this example
|
# The build-arg isn't used; just testing that we accept other args
|
||||||
langgraph build -t ${{ matrix.example.tag }}
|
langgraph build -t langgraph-test-a
|
||||||
# Prepare environment file from local or parent example directory
|
cp .env.example .env
|
||||||
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi
|
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; 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
|
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
|
||||||
# Run the integration test using the built tag
|
- name: Build and test service B
|
||||||
# Compute repo root to reference the shared script robustly
|
if: steps.changed-files.outputs.all
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
working-directory: libs/cli/examples/graphs
|
||||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
|
env:
|
||||||
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
|
run: |
|
||||||
|
langgraph build -t langgraph-test-b
|
||||||
|
cp ../.env.example .env
|
||||||
|
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||||
|
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
|
||||||
|
- name: Build and test service C
|
||||||
|
if: steps.changed-files.outputs.all
|
||||||
|
working-directory: libs/cli/examples/graphs_reqs_a
|
||||||
|
env:
|
||||||
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
|
run: |
|
||||||
|
langgraph build -t langgraph-test-c
|
||||||
|
cp ../.env.example .env
|
||||||
|
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||||
|
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
|
||||||
|
- name: Build and test service D
|
||||||
|
if: steps.changed-files.outputs.all
|
||||||
|
working-directory: libs/cli/examples/graphs_reqs_b
|
||||||
|
env:
|
||||||
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
|
run: |
|
||||||
|
langgraph build -t langgraph-test-d
|
||||||
|
cp ../.env.example .env
|
||||||
|
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||||
|
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
|
||||||
|
|
||||||
- name: Build JS service
|
- name: Build JS service
|
||||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
if: steps.changed-files.outputs.all
|
||||||
working-directory: libs/cli/js-examples
|
working-directory: libs/cli/js-examples
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-e
|
langgraph build -t langgraph-test-e
|
||||||
|
|
||||||
- name: Build JS monorepo service
|
|
||||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
|
||||||
working-directory: libs/cli/js-monorepo-example
|
|
||||||
run: |
|
|
||||||
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
|
|
||||||
|
|
||||||
- name: Build Python monorepo service
|
|
||||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
|
||||||
working-directory: libs/cli/python-monorepo-example
|
|
||||||
run: |
|
|
||||||
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
|
|
||||||
cp apps/agent/.env.example apps/agent/.env
|
|
||||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env; fi
|
|
||||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
|
|
||||||
|
|
||||||
- name: Build and test prerelease reqs service
|
|
||||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
|
||||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
|
||||||
run: |
|
|
||||||
langgraph build -t langgraph-test-h
|
|
||||||
cp ../.env.example .env
|
|
||||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
|
||||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
|
|
||||||
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.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);")
|
|
||||||
if [ "$LANGCHAIN_OPENAI_VERSION" != "1.0.1" ]; then
|
|
||||||
echo "LANGCHAIN_OPENAI_VERSION != 1.0.1; $LANGCHAIN_OPENAI_VERSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
LANGCHAIN_ANTHROPIC_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-anthropic'); print(v);")
|
|
||||||
if [ "$LANGCHAIN_ANTHROPIC_VERSION" != "1.0.0a5" ]; then
|
|
||||||
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.0.0a5; $LANGCHAIN_ANTHROPIC_VERSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and test prerelease reqs fail service
|
|
||||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
|
||||||
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
|
||||||
run: |
|
|
||||||
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- "3.12"
|
- "3.12"
|
||||||
name: "lint #${{ matrix.python-version }}"
|
name: "lint #${{ matrix.python-version }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: Ana06/get-changed-files@v2.3.0
|
uses: Ana06/get-changed-files@v2.3.0
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
filter: "${{ inputs.working-directory }}/**"
|
filter: "${{ inputs.working-directory }}/**"
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
run: uv sync --frozen --group lint
|
run: uv sync --frozen --group dev
|
||||||
|
|
||||||
- name: Get .mypy_cache to speed up mypy
|
- name: Get .mypy_cache to speed up mypy
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
- name: Install test dependencies
|
- name: Install test dependencies
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
run: uv sync --group lint
|
run: uv sync --group dev
|
||||||
|
|
||||||
- name: Get .mypy_cache_test to speed up mypy
|
- name: Get .mypy_cache_test to speed up mypy
|
||||||
if: steps.changed-files.outputs.all
|
if: steps.changed-files.outputs.all
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
|
- "3.9"
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
- "3.14"
|
|
||||||
|
|
||||||
name: "test #${{ matrix.python-version }}"
|
name: "test #${{ matrix.python-version }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
run: uv sync --frozen --group test --no-dev
|
run: uv sync --frozen --group dev
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
|
- "3.9"
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
- "3.14"
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: libs/langgraph
|
working-directory: libs/langgraph
|
||||||
name: "test #${{ matrix.python-version }}"
|
name: "test #${{ matrix.python-version }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: uv sync --frozen --group test --no-dev
|
run: uv sync --frozen --group dev
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
@@ -23,10 +24,10 @@ jobs:
|
|||||||
version: ${{ steps.check-version.outputs.version }}
|
version: ${{ steps.check-version.outputs.version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python $${ env.PYTHON_VERSION }}
|
- name: Set up Python $${ env.PYTHON_VERSION }}
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -48,7 +49,7 @@ jobs:
|
|||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-dist
|
name: test-dist
|
||||||
path: ${{ inputs.working-directory }}/dist/
|
path: ${{ inputs.working-directory }}/dist/
|
||||||
@@ -74,9 +75,9 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-dist
|
name: test-dist
|
||||||
path: ${{ inputs.working-directory }}/dist/
|
path: ${{ inputs.working-directory }}/dist/
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: libs/langgraph
|
working-directory: libs/langgraph
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
|
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "bench"
|
cache-suffix: "bench"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: uv sync --group test
|
run: uv sync --group dev
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
|
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
|
||||||
- name: Save outputs
|
- name: Save outputs
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: libs/langgraph
|
working-directory: libs/langgraph
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- id: files
|
- id: files
|
||||||
name: Get changed files
|
name: Get changed files
|
||||||
uses: Ana06/get-changed-files@v2.3.0
|
uses: Ana06/get-changed-files@v2.3.0
|
||||||
with:
|
with:
|
||||||
format: json
|
format: json
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "bench"
|
cache-suffix: "bench"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: uv sync --group test
|
run: uv sync --group dev
|
||||||
- name: Download baseline
|
- name: Download baseline
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
echo EOF
|
echo EOF
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
- name: Annotation
|
- name: Annotation
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
|
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
python: ${{ steps.filter.outputs.python }}
|
python: ${{ steps.filter.outputs.python }}
|
||||||
deps: ${{ steps.filter.outputs.deps }}
|
deps: ${{ steps.filter.outputs.deps }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
@@ -100,9 +100,9 @@ jobs:
|
|||||||
name: "Check SDK methods matching"
|
name: "Check SDK methods matching"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- name: Run check_sdk_methods script
|
- name: Run check_sdk_methods script
|
||||||
@@ -118,9 +118,9 @@ jobs:
|
|||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
- "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
name: Deploy Docs Redirects
|
name: Deploy Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -20,18 +23,30 @@ defaults:
|
|||||||
working-directory: docs
|
working-directory: docs
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
get-changed-files:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
changed-files: ${{ steps.changed-files.outputs.added_modified }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: Ana06/get-changed-files@v2.3.0
|
||||||
|
with:
|
||||||
|
filter: "docs/docs/**"
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -47,22 +62,85 @@ jobs:
|
|||||||
uv run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
uv run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
# Run unit tests on the docs build pipeline
|
||||||
|
run: make tests
|
||||||
|
- name: Lint Docs
|
||||||
|
# This step lints the docs using the existing linting set up.
|
||||||
|
# It should be very fast and should not require any external services.
|
||||||
|
run: make lint-docs
|
||||||
- name: Build llms-text
|
- name: Build llms-text
|
||||||
run: make llms-text
|
run: make llms-text
|
||||||
|
- name: Build site
|
||||||
- name: Build site (redirects only)
|
run: |
|
||||||
run: make build-docs
|
# If this is main branch, then we want to download stats. we do this
|
||||||
|
# with the env variable DOWNLOAD_STATS=true
|
||||||
|
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||||
|
DOWNLOAD_STATS=true make build-docs
|
||||||
|
else
|
||||||
|
make build-docs
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
|
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||||
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
||||||
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
||||||
|
- name: Check links in notebooks
|
||||||
|
env:
|
||||||
|
LANGCHAIN_API_KEY: test
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "schedule" ]; then
|
||||||
|
echo "Running link check on all HTML files matching notebooks in docs directory..."
|
||||||
|
uv run pytest -v \
|
||||||
|
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||||
|
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||||
|
--check-links-ignore "https://x.com/.*" \
|
||||||
|
--check-links-ignore "https://twitter.com/.*" \
|
||||||
|
--check-links-ignore "https://github\.com/.*" \
|
||||||
|
--check-links-ignore "http://localhost:8123/.*" \
|
||||||
|
--check-links-ignore "http://localhost:2024.*" \
|
||||||
|
--check-links-ignore "http://127.0.0.1:.*" \
|
||||||
|
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||||
|
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||||
|
--check-links-ignore "https://openai\.com/.*" \
|
||||||
|
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||||
|
--check-links-ignore "https://pepy\.tech/.*" \
|
||||||
|
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||||
|
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Fetching changes from origin/main..."
|
||||||
|
git fetch origin main
|
||||||
|
echo "Checking for changed notebook files..."
|
||||||
|
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
|
||||||
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
|
if [ -n "${CHANGED_FILES}" ]; then
|
||||||
|
echo "Running link check on HTML files matching changed notebook files..."
|
||||||
|
uv run pytest -v \
|
||||||
|
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||||
|
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||||
|
--check-links-ignore "http://localhost:8123/.*" \
|
||||||
|
--check-links-ignore "http://localhost:2024.*" \
|
||||||
|
--check-links-ignore "http://127.0.0.1:.*" \
|
||||||
|
--check-links-ignore "https://x.com/.*" \
|
||||||
|
--check-links-ignore "https://twitter.com/.*" \
|
||||||
|
--check-links-ignore "https://github\.com/.*" \
|
||||||
|
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||||
|
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||||
|
--check-links ${CHANGED_FILES} \
|
||||||
|
|| ([ $? = 5 ] && exit 0 || exit $?)
|
||||||
|
else
|
||||||
|
echo "No notebook files changed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Configure GitHub Pages
|
- name: Configure GitHub Pages
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
- name: Upload Pages Artifact
|
- name: Upload Pages Artifact
|
||||||
if: github.ref == 'refs/heads/main'
|
# if: github.ref == 'refs/heads/main'
|
||||||
uses: actions/upload-pages-artifact@v4
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: ./docs/site/
|
path: ./docs/site/
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR Title
|
- name: Validate PR Title
|
||||||
uses: amannn/action-semantic-pull-request@v6
|
uses: amannn/action-semantic-pull-request@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -40,7 +40,6 @@ jobs:
|
|||||||
sdk-py
|
sdk-py
|
||||||
docs
|
docs
|
||||||
ci
|
ci
|
||||||
deps
|
|
||||||
requireScope: false
|
requireScope: false
|
||||||
ignoreLabels: |
|
ignoreLabels: |
|
||||||
ignore-lint-pr-title
|
ignore-lint-pr-title
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
@@ -25,10 +26,10 @@ jobs:
|
|||||||
tag: ${{ steps.check-version.outputs.tag }}
|
tag: ${{ steps.check-version.outputs.tag }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -50,7 +51,7 @@ jobs:
|
|||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: ${{ inputs.working-directory }}/dist/
|
path: ${{ inputs.working-directory }}/dist/
|
||||||
@@ -86,7 +87,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
release-body: ${{ steps.generate-release-body.outputs.release-body }}
|
release-body: ${{ steps.generate-release-body.outputs.release-body }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: langchain-ai/langgraph
|
repository: langchain-ai/langgraph
|
||||||
path: langgraph
|
path: langgraph
|
||||||
@@ -157,7 +158,7 @@ jobs:
|
|||||||
- test-pypi-publish
|
- test-pypi-publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# We explicitly *don't* set up caching here. This ensures our tests are
|
# We explicitly *don't* set up caching here. This ensures our tests are
|
||||||
# maximally sensitive to catching breakage.
|
# maximally sensitive to catching breakage.
|
||||||
@@ -173,7 +174,7 @@ jobs:
|
|||||||
# used in the real world.
|
# used in the real world.
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -221,7 +222,7 @@ jobs:
|
|||||||
uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
|
uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
|
||||||
|
|
||||||
- name: Import test dependencies
|
- name: Import test dependencies
|
||||||
run: uv sync --group test
|
run: uv sync --group dev
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
# Overwrite the local version of the package with the test PyPI version.
|
# Overwrite the local version of the package with the test PyPI version.
|
||||||
@@ -260,16 +261,16 @@ jobs:
|
|||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "release"
|
cache-suffix: "release"
|
||||||
|
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: ${{ inputs.working-directory }}/dist/
|
path: ${{ inputs.working-directory }}/dist/
|
||||||
@@ -301,16 +302,16 @@ jobs:
|
|||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "release"
|
cache-suffix: "release"
|
||||||
|
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: ${{ inputs.working-directory }}/dist/
|
path: ${{ inputs.working-directory }}/dist/
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ jobs:
|
|||||||
- "latest"
|
- "latest"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python + Poetry
|
- name: Set up Python + Poetry
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
# use minimum supported Python version
|
# use minimum supported Python version
|
||||||
python-version: "3.10"
|
python-version: "3.9"
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-suffix: "uv-lock-upgrade"
|
cache-suffix: "uv-lock-upgrade"
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ jobs:
|
|||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
commit-message: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||||
title: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
title: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||||
body: |
|
body: |
|
||||||
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
|
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Below is a high-level overview:
|
|||||||
- **langgraph** – core framework for building stateful, multi-actor agents.
|
- **langgraph** – core framework for building stateful, multi-actor agents.
|
||||||
- **prebuilt** – high-level APIs for creating and running agents and tools.
|
- **prebuilt** – high-level APIs for creating and running agents and tools.
|
||||||
- **sdk-js** – JS/TS SDK for interacting with the LangGraph REST API.
|
- **sdk-js** – JS/TS SDK for interacting with the LangGraph REST API.
|
||||||
- **sdk-py** – Python SDK for the LangGraph Server API.
|
- **sdk-py** – Python SDK for the LangGraph Platform API.
|
||||||
|
|
||||||
### Dependency map
|
### Dependency map
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -277,9 +277,9 @@ def my_function(arg1: int, arg2: str) -> float:
|
|||||||
Examples:
|
Examples:
|
||||||
This is a section for examples of how to use the function.
|
This is a section for examples of how to use the function.
|
||||||
|
|
||||||
```python
|
.. code-block:: python
|
||||||
|
|
||||||
my_function(1, "hello")
|
my_function(1, "hello")
|
||||||
\```
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
arg1: This is a description of arg1. We do not need to specify the type since
|
arg1: This is a description of arg1. We do not need to specify the type since
|
||||||
|
|||||||
@@ -63,15 +63,15 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
|
|||||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||||
|
|
||||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||||
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||||
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
|
- [LangChain](https://python.langchain.com/docs/introduction/) – Provides integrations and composable components to streamline LLM application development.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||||
|
|
||||||
## Additional resources
|
## Additional resources
|
||||||
|
|
||||||
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
- [Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||||
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||||
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
|
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
|
||||||
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
|
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
|
||||||
|
|||||||
+14
-117
@@ -1,126 +1,24 @@
|
|||||||
# LangGraph Documentation
|
# Setup
|
||||||
|
|
||||||
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md).
|
To setup requirements for building docs you can run:
|
||||||
|
|
||||||
## Structure
|
```bash
|
||||||
|
uv sync --group test
|
||||||
The primary documentation is located in the `docs/` directory. This directory contains both the source files for the main documentation as well as the API reference doc build process.
|
```
|
||||||
|
|
||||||
### Main Documentation
|
## Serving documentation locally
|
||||||
|
|
||||||
Main documentation files are located in `docs/docs/` and are written in Markdown format. The site uses [**MkDocs**](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) and includes:
|
To run the documentation server locally you can run:
|
||||||
|
|
||||||
- **Concepts**: Core LangGraph concepts and explanations
|
|
||||||
- **Tutorials**: Step-by-step learning guides
|
|
||||||
- **How-tos**: Task-focused guides for specific use cases
|
|
||||||
- **Examples**: Real-world applications and use cases
|
|
||||||
- **Jupyter Notebooks**: Interactive tutorials that are automatically converted to markdown
|
|
||||||
|
|
||||||
### API Reference
|
|
||||||
|
|
||||||
API reference documentation is defined in `docs/docs/reference/`. Each `.md` file outlines the "template" that each page is built from. Reference content is automatically generated from docstrings in the codebase using the **mkdocstrings** plugin. Once generated, the content is plugged into the corresponding markdown file where it is referenced by using manual directives to specify which classes and/or functions are documented:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
::: langgraph.graph.state.StateGraph
|
|
||||||
options:
|
|
||||||
show_if_no_docstring: true
|
|
||||||
show_root_heading: true
|
|
||||||
show_root_full_path: false
|
|
||||||
members:
|
|
||||||
- add_node
|
|
||||||
- add_edge
|
|
||||||
- add_conditional_edges
|
|
||||||
- add_sequence
|
|
||||||
- compile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Process
|
|
||||||
|
|
||||||
Docs are built following these steps:
|
|
||||||
|
|
||||||
1. **Content Processing:**
|
|
||||||
- `_scripts/notebook_hooks.py` - Main processing pipeline that:
|
|
||||||
- Converts how-tos/tutorial Jupyter notebooks to markdown using `notebook_convert.py`
|
|
||||||
- Adds automatic API reference links to code blocks using `generate_api_reference_links.py`
|
|
||||||
- Handles conditional rendering for Python/JS versions
|
|
||||||
- Processes highlight comments and custom syntax
|
|
||||||
|
|
||||||
2. **API Reference Generation:**
|
|
||||||
- **mkdocstrings** plugin extracts docstrings from Python source code
|
|
||||||
- Manual `::: module.Class` directives in reference pages (`/docs/docs/*`) specify what to document
|
|
||||||
- Cross-references are automatically generated between docs and API
|
|
||||||
|
|
||||||
3. **Site Generation:**
|
|
||||||
- **MkDocs** processes all markdown files and generates static HTML
|
|
||||||
- Custom hooks handle redirects and inject additional functionality
|
|
||||||
|
|
||||||
4. **Deployment:**
|
|
||||||
- Site is deployed with Vercel
|
|
||||||
- `make build-docs` generates production build (also usable for local testing)
|
|
||||||
- Automatic redirects handle URL changes between versions
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
For local development, use the Makefile targets:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Serve docs locally with hot reloading
|
|
||||||
make serve-docs
|
make serve-docs
|
||||||
|
|
||||||
# Clean build for production testing
|
|
||||||
make build-docs
|
|
||||||
|
|
||||||
# Serve with clean build
|
|
||||||
make serve-clean-docs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `serve-docs` command:
|
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
|
||||||
|
|
||||||
- Watches source files for changes
|
|
||||||
- Includes dirty builds for faster iteration
|
|
||||||
- Serves on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/)
|
|
||||||
|
|
||||||
## Standards
|
|
||||||
|
|
||||||
**Docstring Format:**
|
|
||||||
The API reference uses **Google-style docstrings** with Markdown markup. The `mkdocstrings` plugin processes these to generate documentation.
|
|
||||||
|
|
||||||
**Required format:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
def example_function(param1: str, param2: int = 5) -> bool:
|
|
||||||
"""Brief description of the function.
|
|
||||||
|
|
||||||
Longer description can go here. Use Markdown syntax for
|
|
||||||
rich formatting like **bold** and *italic*.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
param1: Description of the first parameter.
|
|
||||||
param2: Description of the second parameter with default value.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Description of the return value.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: When param1 is empty.
|
|
||||||
TypeError: When param2 is not an integer.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
This function is experimental and may change.
|
|
||||||
|
|
||||||
!!! version-added "Added in version 0.2.0"
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Special Markers:**
|
|
||||||
|
|
||||||
- **MkDocs admonitions**: `!!! warning`, `!!! note`, `!!! version-added`
|
|
||||||
- **Code blocks**: Standard markdown ``` syntax
|
|
||||||
- **Cross-references**: Automatic linking via `generate_api_reference_links.py`
|
|
||||||
|
|
||||||
## Execute notebooks
|
## Execute notebooks
|
||||||
|
|
||||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run:
|
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python _scripts/prepare_notebooks_for_ci.py
|
python _scripts/prepare_notebooks_for_ci.py
|
||||||
@@ -135,9 +33,8 @@ python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
|||||||
```
|
```
|
||||||
|
|
||||||
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
|
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
|
||||||
|
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
||||||
- when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
||||||
- when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
|
||||||
|
|
||||||
## Adding new notebooks
|
## Adding new notebooks
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""Generate API reference links for imports in Python code blocks within markdown files."""
|
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
@@ -72,18 +70,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
|||||||
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
|
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
|
||||||
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
|
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
|
||||||
# other prebuilts
|
# other prebuilts
|
||||||
(
|
(["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
|
||||||
["langgraph_supervisor"],
|
(["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
|
||||||
"langgraph_supervisor.supervisor",
|
|
||||||
"create_supervisor",
|
|
||||||
"supervisor",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
["langgraph_supervisor"],
|
|
||||||
"langgraph_supervisor.handoff",
|
|
||||||
"create_handoff_tool",
|
|
||||||
"supervisor",
|
|
||||||
),
|
|
||||||
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
|
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
|
||||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
|
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
|
||||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
|
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _transform_link(
|
def _transform_link(
|
||||||
link_name: str,
|
link_name: str, scope: str, file_path: str, line_number: int, custom_title: Optional[str] = None
|
||||||
scope: str,
|
|
||||||
file_path: str,
|
|
||||||
line_number: int,
|
|
||||||
custom_title: Optional[str] = None,
|
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Transform a cross-reference link based on the current scope.
|
"""Transform a cross-reference link based on the current scope.
|
||||||
|
|
||||||
@@ -42,7 +38,7 @@ def _transform_link(
|
|||||||
scope: The current scope context ("global", "python", "js", etc.).
|
scope: The current scope context ("global", "python", "js", etc.).
|
||||||
file_path: The file path for error reporting.
|
file_path: The file path for error reporting.
|
||||||
line_number: The line number for error reporting.
|
line_number: The line number for error reporting.
|
||||||
custom_title: Optional custom title for the link. If `None`, uses link_name.
|
custom_title: Optional custom title for the link. If None, uses link_name.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A formatted markdown link if the link is found in the scope mapping,
|
A formatted markdown link if the link is found in the scope mapping,
|
||||||
@@ -121,9 +117,7 @@ CROSS_REFERENCE_PATTERN = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _replace_autolinks(
|
def _replace_autolinks(markdown: str, file_path: str, *, default_scope: str = "python") -> str:
|
||||||
markdown: str, file_path: str, *, default_scope: str = "python"
|
|
||||||
) -> str:
|
|
||||||
"""Preprocess markdown lines to handle @[links] with conditional fence scopes.
|
"""Preprocess markdown lines to handle @[links] with conditional fence scopes.
|
||||||
|
|
||||||
This function processes markdown content to transform @[link_name] references
|
This function processes markdown content to transform @[link_name] references
|
||||||
|
|||||||
Binary file not shown.
@@ -2108,9 +2108,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"hono@npm:^4.5.4":
|
"hono@npm:^4.5.4":
|
||||||
version: 4.10.3
|
version: 4.8.9
|
||||||
resolution: "hono@npm:4.10.3"
|
resolution: "hono@npm:4.8.9"
|
||||||
checksum: 10c0/bdcc4c7066c74ba7cfa63ed6550768a0f43a420286c8f8f74b7012ea4901b8b06778fa8e98264b46f1a86920f056b7ede1f07814da4934912f9945def4977c29
|
checksum: 10c0/385539d1787fdc747bc869ef0e5ccc9f39cbe40289b94f23eecfc82c6ca440f059704647cd6381a5066d2cf7baa43ab25184c78d44af4c5c98a5c5b07670059e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""Convert Jupyter notebooks to markdown with custom processing."""
|
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|||||||
+166
-444
@@ -27,413 +27,185 @@ DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
|||||||
|
|
||||||
REDIRECT_MAP = {
|
REDIRECT_MAP = {
|
||||||
# lib redirects
|
# lib redirects
|
||||||
"how-tos/stream-values.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/stream-values.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||||
"how-tos/stream-updates.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/stream-updates.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||||
"how-tos/streaming-content.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/streaming-content.ipynb": "how-tos/streaming.md",
|
||||||
"how-tos/stream-multiple.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/stream-multiple.ipynb": "how-tos/streaming.md#stream-multiple-nodes",
|
||||||
"how-tos/streaming-tokens-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming.md#use-with-any-llm",
|
||||||
"how-tos/streaming-from-final-node.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||||
# graph-api
|
# graph-api
|
||||||
"how-tos/state-reducers.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-and-update-state",
|
"how-tos/state-reducers.ipynb": "how-tos/graph-api.md#define-and-update-state",
|
||||||
"how-tos/sequence.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-a-sequence-of-steps",
|
"how-tos/sequence.ipynb": "how-tos/graph-api.md#create-a-sequence-of-steps",
|
||||||
"how-tos/branching.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-branches",
|
"how-tos/branching.ipynb": "how-tos/graph-api.md#create-branches",
|
||||||
"how-tos/recursion-limit.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-and-control-loops",
|
"how-tos/recursion-limit.ipynb": "how-tos/graph-api.md#create-and-control-loops",
|
||||||
"how-tos/visualization.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#visualize-your-graph",
|
"how-tos/visualization.ipynb": "how-tos/graph-api.md#visualize-your-graph",
|
||||||
"how-tos/input_output_schema.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-input-and-output-schemas",
|
"how-tos/input_output_schema.ipynb": "how-tos/graph-api.md#define-input-and-output-schemas",
|
||||||
"how-tos/pass_private_state.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#pass-private-state-between-nodes",
|
"how-tos/pass_private_state.ipynb": "how-tos/graph-api.md#pass-private-state-between-nodes",
|
||||||
"how-tos/state-model.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#use-pydantic-models-for-graph-state",
|
"how-tos/state-model.ipynb": "how-tos/graph-api.md#use-pydantic-models-for-graph-state",
|
||||||
"how-tos/map-reduce.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#map-reduce-and-the-send-api",
|
"how-tos/map-reduce.ipynb": "how-tos/graph-api.md#map-reduce-and-the-send-api",
|
||||||
"how-tos/command.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#combine-control-flow-and-state-updates-with-command",
|
"how-tos/command.ipynb": "how-tos/graph-api.md#combine-control-flow-and-state-updates-with-command",
|
||||||
"how-tos/configuration.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-runtime-configuration",
|
"how-tos/configuration.ipynb": "how-tos/graph-api.md#add-runtime-configuration",
|
||||||
"how-tos/node-retries.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-retry-policies",
|
"how-tos/node-retries.ipynb": "how-tos/graph-api.md#add-retry-policies",
|
||||||
"how-tos/return-when-recursion-limit-hits.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#impose-a-recursion-limit",
|
"how-tos/return-when-recursion-limit-hits.ipynb": "how-tos/graph-api.md#impose-a-recursion-limit",
|
||||||
"how-tos/async.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#async",
|
"how-tos/async.ipynb": "how-tos/graph-api.md#async",
|
||||||
# memory how-tos
|
# memory how-tos
|
||||||
"how-tos/memory/manage-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"how-tos/memory/manage-conversation-history.ipynb": "how-tos/memory/add-memory.md",
|
||||||
"how-tos/memory/delete-messages.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#delete-messages",
|
"how-tos/memory/delete-messages.ipynb": "how-tos/memory/add-memory.md#delete-messages",
|
||||||
"how-tos/memory/add-summary-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#summarize-messages",
|
"how-tos/memory/add-summary-conversation-history.ipynb": "how-tos/memory/add-memory.md#summarize-messages",
|
||||||
"how-tos/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"how-tos/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||||
"agents/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"agents/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||||
# subgraph how-tos
|
# subgraph how-tos
|
||||||
"how-tos/subgraph-transform-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#different-state-schemas",
|
"how-tos/subgraph-transform-state.ipynb": "how-tos/subgraph.md#different-state-schemas",
|
||||||
"how-tos/subgraphs-manage-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#add-persistence",
|
"how-tos/subgraphs-manage-state.ipynb": "how-tos/subgraph.md#add-persistence",
|
||||||
# persistence how-tos
|
# persistence how-tos
|
||||||
"how-tos/persistence_postgres.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
"how-tos/persistence_postgres.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||||
"how-tos/persistence_mongodb.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
"how-tos/persistence_mongodb.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||||
"how-tos/persistence_redis.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
"how-tos/persistence_redis.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||||
"how-tos/subgraph-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-with-subgraphs",
|
"how-tos/subgraph-persistence.ipynb": "how-tos/memory/add-memory.md#use-with-subgraphs",
|
||||||
"how-tos/cross-thread-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
|
"how-tos/cross-thread-persistence.ipynb": "how-tos/memory/add-memory.md#add-long-term-memory",
|
||||||
"cloud/how-tos/copy_threads": "https://docs.langchain.com/langsmith/use-threads",
|
"cloud/how-tos/copy_threads": "cloud/how-tos/use_threads",
|
||||||
"cloud/how-tos/check-thread-status": "https://docs.langchain.com/langsmith/use-threads",
|
"cloud/how-tos/check-thread-status": "cloud/how-tos/use_threads",
|
||||||
"cloud/concepts/threads.md": "https://docs.langchain.com/oss/python/langgraph/persistence#threads",
|
"cloud/concepts/threads.md": "concepts/persistence.md#threads",
|
||||||
"how-tos/persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"how-tos/persistence.ipynb": "how-tos/memory/add-memory.md",
|
||||||
# tool calling how-tos
|
# tool calling how-tos
|
||||||
"how-tos/tool-calling-errors.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
"how-tos/tool-calling-errors.ipynb": "how-tos/tool-calling.ipynb#handle-errors",
|
||||||
"how-tos/pass-config-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
"how-tos/pass-config-to-tools.ipynb": "how-tos/tool-calling.ipynb#access-config",
|
||||||
"how-tos/pass-run-time-values-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
"how-tos/pass-run-time-values-to-tools.ipynb": "how-tos/tool-calling.ipynb#read-state",
|
||||||
"how-tos/update-state-from-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
"how-tos/update-state-from-tools.ipynb": "how-tos/tool-calling.ipynb#update-state",
|
||||||
"agents/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
"agents/tools.md": "how-tos/tool-calling.md",
|
||||||
# multi-agent how-tos
|
# multi-agent how-tos
|
||||||
"how-tos/agent-handoffs.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
"how-tos/agent-handoffs.ipynb": "how-tos/multi_agent.md#handoffs",
|
||||||
"how-tos/multi-agent-network.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
"how-tos/multi-agent-network.ipynb": "how-tos/multi_agent.md#use-in-a-multi-agent-system",
|
||||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
"how-tos/multi-agent-multi-turn-convo.ipynb": "how-tos/multi_agent.md#multi-turn-conversation",
|
||||||
# cloud redirects
|
# cloud redirects
|
||||||
"cloud/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
"cloud/index.md": "index.md",
|
||||||
"cloud/how-tos/index.md": "https://docs.langchain.com/langsmith/home",
|
"cloud/how-tos/index.md": "concepts/langgraph_platform",
|
||||||
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/agent-server",
|
"cloud/concepts/api.md": "concepts/langgraph_server.md",
|
||||||
"cloud/concepts/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
|
||||||
"cloud/faq/studio.md": "https://docs.langchain.com/langsmith/studio",
|
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
|
||||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
"cloud/how-tos/human_in_the_loop_edit_state.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||||
"cloud/how-tos/human_in_the_loop_user_input.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
"cloud/how-tos/human_in_the_loop_user_input.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||||
"concepts/platform_architecture.md": "https://docs.langchain.com/langsmith/cloud#architecture",
|
"concepts/platform_architecture.md": "concepts/langgraph_cloud#architecture",
|
||||||
# cloud streaming redirects
|
# cloud streaming redirects
|
||||||
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"cloud/concepts/streaming.md": "concepts/streaming.md",
|
||||||
"agents/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
"agents/streaming.md": "how-tos/streaming.md",
|
||||||
# prebuilt redirects
|
# prebuilt redirects
|
||||||
"how-tos/create-react-agent.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#basic-configuration",
|
"how-tos/create-react-agent.ipynb": "agents/agents.md#basic-configuration",
|
||||||
"how-tos/create-react-agent-memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"how-tos/create-react-agent-memory.ipynb": "agents/memory.md",
|
||||||
"how-tos/create-react-agent-system-prompt.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
"how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts",
|
||||||
"how-tos/create-react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
|
"how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output",
|
||||||
# misc
|
# misc
|
||||||
"prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
"prebuilt.md": "agents/prebuilt.md",
|
||||||
"reference/prebuilt.md": "https://reference.langchain.com/python/langgraph/agents/",
|
"reference/prebuilt.md": "reference/agents.md",
|
||||||
"concepts/high_level.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
"concepts/high_level.md": "index.md",
|
||||||
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
"concepts/index.md": "index.md",
|
||||||
"concepts/v0-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"concepts/v0-human-in-the-loop.md": "concepts/human-in-the-loop.md",
|
||||||
"how-tos/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
"how-tos/index.md": "index.md",
|
||||||
"tutorials/introduction.ipynb": "https://docs.langchain.com/oss/python/langgraph/overview",
|
"tutorials/introduction.ipynb": "concepts/why-langgraph.md",
|
||||||
"agents/deployment.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
"agents/deployment.md": "tutorials/langgraph-platform/local-server.md",
|
||||||
# deployment redirects
|
# deployment redirects
|
||||||
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
|
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
|
||||||
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
|
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
|
||||||
"tutorials/deployment.md": "https://docs.langchain.com/langsmith/deployments",
|
"tutorials/deployment.md": "concepts/deployment_options.md",
|
||||||
# assistant redirects
|
# assistant redirects
|
||||||
"cloud/how-tos/assistant_versioning.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
"cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md",
|
||||||
"cloud/concepts/runs.md": "https://docs.langchain.com/langsmith/assistants#execution",
|
"cloud/concepts/runs.md": "concepts/assistants.md#execution",
|
||||||
# hitl redirects
|
# hitl redirects
|
||||||
"how-tos/wait-user-input-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
"how-tos/wait-user-input-functional.ipynb": "how-tos/use-functional-api.md",
|
||||||
"how-tos/review-tool-calls-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
"how-tos/review-tool-calls-functional.ipynb": "how-tos/use-functional-api.md",
|
||||||
"how-tos/create-react-agent-hitl.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"how-tos/create-react-agent-hitl.ipynb": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||||
"agents/human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"agents/human-in-the-loop.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||||
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "how-tos/human_in_the_loop/breakpoints.md",
|
||||||
"concepts/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"concepts/breakpoints.md": "concepts/human_in_the_loop.md",
|
||||||
"how-tos/human_in_the_loop/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
"how-tos/human_in_the_loop/breakpoints.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||||
"cloud/how-tos/human_in_the_loop_breakpoint.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
"cloud/how-tos/human_in_the_loop_breakpoint.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||||
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
|
||||||
|
|
||||||
# LGP mintlify migration redirects
|
# LGP mintlify migration redirects
|
||||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langgraph-platform/auth",
|
||||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
|
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langgraph-platform/resource-auth",
|
||||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
|
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langgraph-platform/add-auth-server",
|
||||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
|
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langgraph-platform/use-remote-graph",
|
||||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
|
"how-tos/autogen-integration.md": "https://docs.langchain.com/langgraph-platform/autogen-integration",
|
||||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
|
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langgraph-platform/use-stream-react",
|
||||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langgraph-platform/generative-ui-react",
|
||||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
|
"concepts/langgraph_platform.md": "https://docs.langchain.com/langgraph-platform/index",
|
||||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
"concepts/langgraph_components.md": "https://docs.langchain.com/langgraph-platform/components",
|
||||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
|
"concepts/langgraph_server.md": "https://docs.langchain.com/langgraph-platform/langgraph-server",
|
||||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langgraph-platform/data-plane",
|
||||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langgraph-platform/control-plane",
|
||||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
|
||||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
|
||||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
|
||||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio#run-application",
|
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
|
||||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio#manage-assistants",
|
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
|
||||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-studio#manage-threads",
|
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
|
||||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/observability-studio#iterate-on-prompts",
|
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
|
||||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability-studio#run-experiments-over-a-dataset",
|
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
|
||||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability-studio#debug-langsmith-traces",
|
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
|
||||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/observability-studio#add-node-to-dataset",
|
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langgraph-platform/datasets-studio",
|
||||||
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
|
"concepts/sdk.md": "https://docs.langchain.com/langgraph-platform/sdk",
|
||||||
"concepts/plans.md": "https://langchain.com/pricing",
|
"concepts/plans.md": "https://docs.langchain.com/langgraph-platform/plans",
|
||||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
"concepts/application_structure.md": "https://docs.langchain.com/langgraph-platform/application-structure",
|
||||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langgraph-platform/scalability-and-resilience",
|
||||||
"concepts/auth.md": "https://docs.langchain.com/langsmith/authentication-methods",
|
"concepts/auth.md": "https://docs.langchain.com/langgraph-platform/auth",
|
||||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
|
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langgraph-platform/custom-auth",
|
||||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
|
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langgraph-platform/openapi-security",
|
||||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
"concepts/assistants.md": "https://docs.langchain.com/langgraph-platform/assistants",
|
||||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langgraph-platform/configuration-cloud",
|
||||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
|
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langgraph-platform/use-threads",
|
||||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
|
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langgraph-platform/background-run",
|
||||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
|
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langgraph-platform/same-thread",
|
||||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
|
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langgraph-platform/stateless-runs",
|
||||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
|
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langgraph-platform/configurable-headers",
|
||||||
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
|
"concepts/double_texting.md": "https://docs.langchain.com/langgraph-platform/double-texting",
|
||||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langgraph-platform/interrupt-concurrent",
|
||||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langgraph-platform/rollback-concurrent",
|
||||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
|
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langgraph-platform/reject-concurrent",
|
||||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langgraph-platform/enqueue-concurrent",
|
||||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
|
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langgraph-platform/custom-lifespan",
|
||||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
|
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langgraph-platform/custom-middleware",
|
||||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
|
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langgraph-platform/custom-routes",
|
||||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langgraph-platform/data-storage-and-privacy",
|
||||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langgraph-platform/semantic-search",
|
||||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langgraph-platform/configure-ttl",
|
||||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/platform-setup",
|
"concepts/deployment_options.md": "https://docs.langchain.com/langgraph-platform/deployment-options",
|
||||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
"cloud/quick_start.md": "https://docs.langchain.com/langgraph-platform/deployment-quickstart",
|
||||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
"cloud/deployment/setup.md": "https://docs.langchain.com/langgraph-platform/setup-app-requirements-txt",
|
||||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langgraph-platform/setup-pyproject",
|
||||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langgraph-platform/setup-javascript",
|
||||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langgraph-platform/custom-docker",
|
||||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langgraph-platform/graph-rebuild",
|
||||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hybrid",
|
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid",
|
||||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/self-hosted",
|
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted",
|
||||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/self-hosted#standalone-server",
|
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#standalone-server",
|
||||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
"cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/deploy-hybrid",
|
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid",
|
||||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/deploy-self-hosted-full-platform",
|
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform",
|
||||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/deploy-standalone-server",
|
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-standalone-server",
|
||||||
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
|
"concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp",
|
||||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel",
|
||||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop",
|
||||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
"cloud/deployment/egress.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langgraph-platform/server-api-ref",
|
||||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
|
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langgraph-platform/langgraph-server-changelog",
|
||||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langgraph-platform/api-ref-control-plane",
|
||||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
"cloud/reference/cli.md": "https://docs.langchain.com/langgraph-platform/cli",
|
||||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
"cloud/reference/env_var.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
"troubleshooting/studio.md": "https://docs.langchain.com/langgraph-platform/troubleshooting-studio",
|
||||||
|
|
||||||
# LangGraph mintlify migration redirects
|
|
||||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"tutorials/get-started/1-build-basic-chatbot.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/get-started/2-add-tools.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/get-started/3-add-memory.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/get-started/4-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/get-started/5-customize-state.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
|
||||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
|
||||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
|
||||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
|
||||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
|
||||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
|
||||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
|
||||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
|
||||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
|
||||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
|
||||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
|
||||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
|
||||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
|
||||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
|
||||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
|
||||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
|
||||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
|
||||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
|
||||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
|
||||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
|
||||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
|
||||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
|
||||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
|
||||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
|
||||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
|
||||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
"reference/index.md": "https://reference.langchain.com/python/langgraph/",
|
|
||||||
"reference/graphs.md": "https://reference.langchain.com/python/langgraph/graphs/",
|
|
||||||
"reference/func.md": "https://reference.langchain.com/python/langgraph/func/",
|
|
||||||
"reference/pregel.md": "https://reference.langchain.com/python/langgraph/pregel/",
|
|
||||||
"reference/checkpoints.md": "https://reference.langchain.com/python/langgraph/checkpoints/",
|
|
||||||
"reference/store.md": "https://reference.langchain.com/python/langgraph/store/",
|
|
||||||
"reference/cache.md": "https://reference.langchain.com/python/langgraph/cache/",
|
|
||||||
"reference/types.md": "https://reference.langchain.com/python/langgraph/types/",
|
|
||||||
"reference/runtime.md": "https://reference.langchain.com/python/langgraph/runtime/",
|
|
||||||
"reference/config.md": "https://reference.langchain.com/python/langgraph/config/",
|
|
||||||
"reference/errors.md": "https://reference.langchain.com/python/langgraph/errors/",
|
|
||||||
"reference/constants.md": "https://reference.langchain.com/python/langgraph/constants/",
|
|
||||||
"reference/channels.md": "https://reference.langchain.com/python/langgraph/channels/",
|
|
||||||
"reference/agents.md": "https://reference.langchain.com/python/langgraph/agents/",
|
|
||||||
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
|
|
||||||
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
|
|
||||||
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
|
|
||||||
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
|
|
||||||
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
|
|
||||||
|
|
||||||
# additional exclude-search entries from mkdocs.yml
|
|
||||||
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
|
||||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
|
||||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
|
||||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
|
||||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
|
||||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
|
||||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
|
||||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
|
||||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
|
||||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
|
||||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
|
||||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
|
||||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
|
||||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
|
||||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
|
||||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
|
||||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
|
|
||||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability",
|
|
||||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
|
|
||||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
|
||||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
|
||||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
|
||||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
|
||||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
|
||||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
|
||||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
|
||||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
|
||||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
|
||||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
|
|
||||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
|
||||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
|
|
||||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
|
|
||||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
|
||||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio",
|
|
||||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
|
||||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability",
|
|
||||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-threads",
|
|
||||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
|
|
||||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
|
|
||||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
|
||||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
|
||||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
|
||||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
|
||||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
|
||||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
|
||||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
|
|
||||||
"cloud/reference/sdk/js_ts_sdk_ref.md": "https://reference.langchain.com/javascript/modules/langsmith.html",
|
|
||||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
|
||||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
|
||||||
"concepts/auth.md": "https://docs.langchain.com/langsmith/auth",
|
|
||||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/deployments",
|
|
||||||
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
|
|
||||||
"concepts/faq.md": "https://docs.langchain.com/langsmith/faq",
|
|
||||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
|
||||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
|
||||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
|
||||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
|
||||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
|
||||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/home",
|
|
||||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
|
||||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
|
||||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
|
|
||||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
|
||||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
|
||||||
"concepts/plans.md": "https://docs.langchain.com/langsmith/home",
|
|
||||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
|
||||||
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
|
|
||||||
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
|
|
||||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
|
||||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
|
||||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
|
|
||||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
|
|
||||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
|
|
||||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
|
|
||||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
|
|
||||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
|
|
||||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
|
||||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
|
|
||||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"snippets/chat_model_tabs.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
|
||||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
|
||||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
|
||||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
|
||||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
|
||||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
|
||||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
|
||||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
|
|
||||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
|
||||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
|
|
||||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
|
||||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
|
||||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
|
||||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
|
||||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
|
||||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
|
||||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
|
||||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
|
||||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
|
||||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
|
||||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
|
||||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
|
||||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
|
||||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
|
||||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
|
||||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
|
||||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
|
||||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
|
||||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
|
||||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
|
||||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
|
||||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
|
||||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
|
||||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
|
||||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
|
||||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
|
||||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
|
||||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
|
||||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
|
||||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
|
||||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
|
||||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
|
||||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
|
||||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
|
||||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
|
||||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -788,12 +560,6 @@ def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
|
|||||||
# Create HTML files for redirects after site dir has been built
|
# Create HTML files for redirects after site dir has been built
|
||||||
def on_post_build(config):
|
def on_post_build(config):
|
||||||
use_directory_urls = config.get("use_directory_urls")
|
use_directory_urls = config.get("use_directory_urls")
|
||||||
site_dir = config["site_dir"]
|
|
||||||
|
|
||||||
# Track which paths have explicit redirects
|
|
||||||
redirected_paths = set()
|
|
||||||
|
|
||||||
# Process explicit redirects from REDIRECT_MAP
|
|
||||||
for page_old, page_new in REDIRECT_MAP.items():
|
for page_old, page_new in REDIRECT_MAP.items():
|
||||||
# Convert .ipynb to .md for path calculation
|
# Convert .ipynb to .md for path calculation
|
||||||
page_old = page_old.replace(".ipynb", ".md")
|
page_old = page_old.replace(".ipynb", ".md")
|
||||||
@@ -812,12 +578,9 @@ def on_post_build(config):
|
|||||||
else:
|
else:
|
||||||
old_html_path = page_old + ".html"
|
old_html_path = page_old + ".html"
|
||||||
|
|
||||||
# Track this path as redirected
|
|
||||||
redirected_paths.add(old_html_path)
|
|
||||||
|
|
||||||
if isinstance(page_new, str) and page_new.startswith("http"):
|
if isinstance(page_new, str) and page_new.startswith("http"):
|
||||||
# Handle external redirects
|
# Handle external redirects
|
||||||
_write_html(site_dir, old_html_path, page_new)
|
_write_html(config["site_dir"], old_html_path, page_new)
|
||||||
else:
|
else:
|
||||||
# Handle internal redirects
|
# Handle internal redirects
|
||||||
page_new = page_new.replace(".ipynb", ".md")
|
page_new = page_new.replace(".ipynb", ".md")
|
||||||
@@ -845,45 +608,4 @@ def on_post_build(config):
|
|||||||
new_html_path = page_new_before_hash + ".html"
|
new_html_path = page_new_before_hash + ".html"
|
||||||
new_html_path += hash + suffix
|
new_html_path += hash + suffix
|
||||||
|
|
||||||
_write_html(site_dir, old_html_path, new_html_path)
|
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||||
|
|
||||||
# Create root index.html redirect
|
|
||||||
root_redirect_html = """<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Redirecting to LangGraph Documentation</title>
|
|
||||||
<link rel="canonical" href="https://docs.langchain.com/oss/python/langgraph/overview">
|
|
||||||
<meta name="robots" content="noindex">
|
|
||||||
<script>var anchor=window.location.hash.substr(1);location.href="https://docs.langchain.com/oss/python/langgraph/overview"+(anchor?"#"+anchor:"")</script>
|
|
||||||
<meta http-equiv="refresh" content="0; url=https://docs.langchain.com/oss/python/langgraph/overview">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Documentation has moved</h1>
|
|
||||||
<p>The LangGraph documentation has moved to <a href="https://docs.langchain.com/oss/python/langgraph/overview">docs.langchain.com</a>.</p>
|
|
||||||
<p>Redirecting you now...</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
root_index_path = os.path.join(site_dir, "index.html")
|
|
||||||
with open(root_index_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(root_redirect_html)
|
|
||||||
|
|
||||||
# Create server-side catch-all redirect file for Netlify/Cloudflare Pages
|
|
||||||
# This handles any pages not explicitly mapped in REDIRECT_MAP
|
|
||||||
# Note: This won't work on GitHub Pages, but kept for potential future use
|
|
||||||
redirects_content = """# Netlify/Cloudflare Pages redirect rules
|
|
||||||
# Specific redirects are handled by individual HTML redirect pages
|
|
||||||
# This is the catch-all for any unmapped pages
|
|
||||||
|
|
||||||
# Exclude reference docs from catch-all
|
|
||||||
/reference/* 200
|
|
||||||
|
|
||||||
# Catch-all: redirect any page not explicitly mapped
|
|
||||||
/* https://docs.langchain.com/oss/python/langgraph/overview 301
|
|
||||||
"""
|
|
||||||
|
|
||||||
redirects_path = os.path.join(site_dir, "_redirects")
|
|
||||||
with open(redirects_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(redirects_content)
|
|
||||||
|
|||||||
@@ -20,19 +20,16 @@ class Package(TypedDict):
|
|||||||
description: str
|
description: str
|
||||||
"""A brief description of what the package does."""
|
"""A brief description of what the package does."""
|
||||||
|
|
||||||
|
|
||||||
class ResolvedPackage(Package):
|
class ResolvedPackage(Package):
|
||||||
weekly_downloads: int | None
|
weekly_downloads: int | None
|
||||||
"""The weekly download count of the package."""
|
"""The weekly download count of the package."""
|
||||||
language: str
|
language: str
|
||||||
"""The language of the package. (either 'python' or 'js')"""
|
"""The language of the package. (either 'python' or 'js')"""
|
||||||
|
|
||||||
|
|
||||||
HERE = pathlib.Path(__file__).parent
|
HERE = pathlib.Path(__file__).parent
|
||||||
PACKAGES_FILE = HERE / "packages.yml"
|
PACKAGES_FILE = HERE / "packages.yml"
|
||||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())["packages"]
|
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())["packages"]
|
||||||
|
|
||||||
|
|
||||||
def _get_pypi_downloads(package: Package) -> int:
|
def _get_pypi_downloads(package: Package) -> int:
|
||||||
"""Retrieve the weekly download count for a package from PyPIStats."""
|
"""Retrieve the weekly download count for a package from PyPIStats."""
|
||||||
|
|
||||||
@@ -76,7 +73,6 @@ def _get_pypi_downloads(package: Package) -> int:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_npm_downloads(package: Package) -> int:
|
def _get_npm_downloads(package: Package) -> int:
|
||||||
"""Retrieve the weekly download count for a package on the npm registry."""
|
"""Retrieve the weekly download count for a package on the npm registry."""
|
||||||
|
|
||||||
@@ -86,18 +82,14 @@ def _get_npm_downloads(package: Package) -> int:
|
|||||||
npm_response = requests.get(npm_url)
|
npm_response = requests.get(npm_url)
|
||||||
npm_response.raise_for_status()
|
npm_response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
raise AssertionError(
|
raise AssertionError(f"Package {package['name']} does not exist on npm registry")
|
||||||
f"Package {package['name']} does not exist on npm registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
npm_data = npm_response.json()
|
npm_data = npm_response.json()
|
||||||
|
|
||||||
# Retrieve the first publish date using the 'created' timestamp from the 'time' field.
|
# Retrieve the first publish date using the 'created' timestamp from the 'time' field.
|
||||||
created_str = npm_data.get("time", {}).get("created")
|
created_str = npm_data.get("time", {}).get("created")
|
||||||
if created_str is None:
|
if created_str is None:
|
||||||
raise AssertionError(
|
raise AssertionError(f"Package {package['name']} has no creation time in registry data")
|
||||||
f"Package {package['name']} has no creation time in registry data"
|
|
||||||
)
|
|
||||||
# Remove the trailing 'Z' if present and parse the ISO format timestamp
|
# Remove the trailing 'Z' if present and parse the ISO format timestamp
|
||||||
first_publish_date = datetime.fromisoformat(created_str.rstrip("Z"))
|
first_publish_date = datetime.fromisoformat(created_str.rstrip("Z"))
|
||||||
|
|
||||||
@@ -111,10 +103,7 @@ def _get_npm_downloads(package: Package) -> int:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> list[ResolvedPackage]:
|
||||||
def _get_weekly_downloads(
|
|
||||||
packages: dict[str, list[Package]], fake: bool
|
|
||||||
) -> list[ResolvedPackage]:
|
|
||||||
"""Retrieve the weekly download count for a dictionary of python or js packages."""
|
"""Retrieve the weekly download count for a dictionary of python or js packages."""
|
||||||
resolved_packages: list[ResolvedPackage] = []
|
resolved_packages: list[ResolvedPackage] = []
|
||||||
|
|
||||||
@@ -156,13 +145,12 @@ def _get_weekly_downloads(
|
|||||||
|
|
||||||
return resolved_packages
|
return resolved_packages
|
||||||
|
|
||||||
|
|
||||||
def main(output_file: str, fake: bool) -> None:
|
def main(output_file: str, fake: bool) -> None:
|
||||||
"""Main function to generate package download information.
|
"""Main function to generate package download information.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output_file: Path to the output YAML file.
|
output_file: Path to the output YAML file.
|
||||||
fake: If `True`, use fake download counts for testing purposes.
|
fake: If True, use fake download counts for testing purposes.
|
||||||
"""
|
"""
|
||||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
|
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ LangGraph provides three ways to manage context, which combines the mutability a
|
|||||||
|
|
||||||
**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution.
|
**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution.
|
||||||
|
|
||||||
!!! version-added "Added in version 0.6.0: `context` replaces `config['configurable']`"
|
!!! version-added "New in LangGraph v0.6: `context` replaces `config['configurable']`"
|
||||||
|
|
||||||
Runtime context is now passed to the `context` argument of `invoke`/`stream`,
|
Runtime context is now passed to the `context` argument of `invoke`/`stream`,
|
||||||
which replaces the previous pattern of passing application configuration to `config['configurable']`.
|
which replaces the previous pattern of passing application configuration to `config['configurable']`.
|
||||||
@@ -90,7 +90,7 @@ graph.invoke( # (1)!
|
|||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
|
|
||||||
# highlight-next-line
|
# highlight-next-line
|
||||||
def node(state: State, runtime: Runtime[ContextSchema]):
|
def node(state: State, config: Runtime[ContextSchema]):
|
||||||
user_name = runtime.context.user_name
|
user_name = runtime.context.user_name
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ output = agent.invoke(
|
|||||||
print(output["messages"][-1].text())
|
print(output["messages"][-1].text())
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! version-added "Added in version 0.6.0"
|
!!! version-added "New in LangGraph v0.6"
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -351,13 +351,11 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
|||||||
:::python
|
:::python
|
||||||
|
|
||||||
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://python.langchain.com/docs/how_to/custom_chat_model/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://python.langchain.com/docs/how_to/custom_chat_model/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
|
|
||||||
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://js.langchain.com/docs/how_to/custom_chat/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://js.langchain.com/docs/how_to/custom_chat/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
2. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](../how-tos/streaming.md#use-with-any-llm) with `StreamWriter`.
|
2. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](../how-tos/streaming.md#use-with-any-llm) with `StreamWriter`.
|
||||||
@@ -373,7 +371,6 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
|||||||
- [Force model to call a specific tool](https://python.langchain.com/docs/how_to/tool_choice/)
|
- [Force model to call a specific tool](https://python.langchain.com/docs/how_to/tool_choice/)
|
||||||
- [All chat model how-to guides](https://python.langchain.com/docs/how_to/#chat-models)
|
- [All chat model how-to guides](https://python.langchain.com/docs/how_to/#chat-models)
|
||||||
- [Chat model integrations](https://python.langchain.com/docs/integrations/chat/)
|
- [Chat model integrations](https://python.langchain.com/docs/integrations/chat/)
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
@@ -384,5 +381,4 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
|
|||||||
- [Force model to call a specific tool](https://js.langchain.com/docs/how_to/tool_choice/)
|
- [Force model to call a specific tool](https://js.langchain.com/docs/how_to/tool_choice/)
|
||||||
- [All chat model how-to guides](https://js.langchain.com/docs/how_to/#chat-models)
|
- [All chat model how-to guides](https://js.langchain.com/docs/how_to/#chat-models)
|
||||||
- [Chat model integrations](https://js.langchain.com/docs/integrations/chat/)
|
- [Chat model integrations](https://js.langchain.com/docs/integrations/chat/)
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ Starting from the `LangGraph Platform` view...
|
|||||||
1. Update the `Git Branch` to the desired branch.
|
1. Update the `Git Branch` to the desired branch.
|
||||||
1. Check/uncheck checkbox to `Automatically update deployment on push to branch`.
|
1. Check/uncheck checkbox to `Automatically update deployment on push to branch`.
|
||||||
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
|
1. Branch creation/deletion and tag creation/deletion events will not trigger an update. Only pushes to an existing branch will trigger an update.
|
||||||
1. Pushes in quick succession to a branch will queue subsequent updates. Once a build completes, the most recent commit will begin building and the other queued builds will be skipped.
|
1. Pushes in quick succession to a branch will not trigger subsequent updates. In the future, this functionality may be changed/improved.
|
||||||
|
|
||||||
## Add or Remove GitHub Repositories
|
## Add or Remove GitHub Repositories
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "LangSmith Deployment",
|
"title": "LangGraph Platform",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -29,10 +29,6 @@
|
|||||||
"name": "Store",
|
"name": "Store",
|
||||||
"description": "Store is an API for managing persistent key-value store (long-term memory) that is available from any thread."
|
"description": "Store is an API for managing persistent key-value store (long-term memory) that is available from any thread."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "A2A",
|
|
||||||
"description": "Agent-to-Agent Protocol related endpoints for exposing assistants as A2A-compliant agents."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "MCP",
|
"name": "MCP",
|
||||||
"description": "Model Context Protocol related endpoints for exposing an agent as an MCP server."
|
"description": "Model Context Protocol related endpoints for exposing an agent as an MCP server."
|
||||||
@@ -1524,96 +1520,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/threads/{thread_id}/stream": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Threads"
|
|
||||||
],
|
|
||||||
"summary": "Join Thread Stream",
|
|
||||||
"description": "This endpoint streams output in real-time from a thread. The stream will include the output of each run executed sequentially on the thread and will remain open indefinitely. It is the responsibility of the calling client to close the connection.",
|
|
||||||
"operationId": "join_thread_stream_threads__thread_id__stream_get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "The ID of the thread.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"title": "Thread Id",
|
|
||||||
"description": "The ID of the thread."
|
|
||||||
},
|
|
||||||
"name": "thread_id",
|
|
||||||
"in": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Last Event ID",
|
|
||||||
"description": "The ID of the last event received. Used to resume streaming from a specific point. Pass '-' to resume from the beginning."
|
|
||||||
},
|
|
||||||
"name": "Last-Event-ID",
|
|
||||||
"in": "header"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["lifecycle", "run_modes", "state_update"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["lifecycle", "run_modes", "state_update"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default": ["run_modes"],
|
|
||||||
"title": "Stream Modes",
|
|
||||||
"description": "Stream modes to control which events are returned. 'lifecycle' returns only run start/end events, 'run_modes' returns all run events (default behavior), 'state_update' returns only state update events."
|
|
||||||
},
|
|
||||||
"name": "stream_modes",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"text/event-stream": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/threads/{thread_id}/runs": {
|
"/threads/{thread_id}/runs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3186,195 +3092,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/a2a/{assistant_id}": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "post_a2a",
|
|
||||||
"summary": "A2A Post",
|
|
||||||
"description": "Communicate with an assistant using the Agent-to-Agent Protocol.\nSends a JSON-RPC 2.0 message to the assistant.\n\n- **Request**: Provide an object with `jsonrpc`, `id`, `method`, and optional `params`.\n- **Response**: Returns a JSON-RPC response with task information or error.\n\n**Supported Methods:**\n- `message/send`: Send a message to the assistant\n- `tasks/get`: Get the status and result of a task\n\n**Notes:**\n- Supports threaded conversations via thread context\n- Messages can contain text and data parts\n- Tasks run asynchronously and return completion status\n",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "assistant_id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"description": "The ID of the assistant to communicate with"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Accept",
|
|
||||||
"in": "header",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["application/json"]
|
|
||||||
},
|
|
||||||
"description": "Must be application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"jsonrpc": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["2.0"],
|
|
||||||
"description": "JSON-RPC version"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Request identifier"
|
|
||||||
},
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["message/send", "tasks/get"],
|
|
||||||
"description": "The method to invoke"
|
|
||||||
},
|
|
||||||
"params": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Method parameters",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"title": "Message Send Parameters",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["user", "assistant"],
|
|
||||||
"description": "Message role"
|
|
||||||
},
|
|
||||||
"parts": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"title": "Text Part",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"kind": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["text"]
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["kind", "text"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Data Part",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"kind": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["data"]
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["kind", "data"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Message parts"
|
|
||||||
},
|
|
||||||
"messageId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Unique message identifier"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["role", "parts", "messageId"]
|
|
||||||
},
|
|
||||||
"thread": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"threadId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Thread identifier for conversation context"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Optional thread context"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["message"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Task Get Parameters",
|
|
||||||
"properties": {
|
|
||||||
"taskId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Task identifier to retrieve"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["taskId"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["jsonrpc", "id", "method"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "JSON-RPC response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"jsonrpc": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["2.0"]
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"result": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Success result containing task information or task details"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Error information if request failed"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["jsonrpc", "id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad request - invalid JSON-RPC or missing Accept header"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Assistant not found"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal server error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"A2A"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/mcp/": {
|
"/mcp/": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "post_mcp",
|
"operationId": "post_mcp",
|
||||||
@@ -4629,17 +4346,6 @@
|
|||||||
"title": "Checkpoint During",
|
"title": "Checkpoint During",
|
||||||
"description": "Whether to checkpoint during the run.",
|
"description": "Whether to checkpoint during the run.",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
|
||||||
"durability": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"sync",
|
|
||||||
"async",
|
|
||||||
"exit"
|
|
||||||
],
|
|
||||||
"title": "Durability",
|
|
||||||
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
|
|
||||||
"default": "async"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -4876,17 +4582,6 @@
|
|||||||
"title": "Checkpoint During",
|
"title": "Checkpoint During",
|
||||||
"description": "Whether to checkpoint during the run.",
|
"description": "Whether to checkpoint during the run.",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
|
||||||
"durability": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"sync",
|
|
||||||
"async",
|
|
||||||
"exit"
|
|
||||||
],
|
|
||||||
"title": "Durability",
|
|
||||||
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
|
|
||||||
"default": "async"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -5015,12 +4710,6 @@
|
|||||||
},
|
},
|
||||||
"ThreadSearchRequest": {
|
"ThreadSearchRequest": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ids": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string", "format": "uuid"},
|
|
||||||
"title": "Ids",
|
|
||||||
"description": "List of thread IDs to include. Others are excluded."
|
|
||||||
},
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Metadata",
|
"title": "Metadata",
|
||||||
@@ -5261,30 +4950,11 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Metadata",
|
"title": "Metadata",
|
||||||
"description": "Metadata to merge with existing thread metadata."
|
"description": "Metadata to merge with existing thread metadata."
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "TTL",
|
|
||||||
"description": "The time-to-live for the thread.",
|
|
||||||
"properties": {
|
|
||||||
"strategy": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"delete"
|
|
||||||
],
|
|
||||||
"description": "The TTL strategy. 'delete' removes the entire thread.",
|
|
||||||
"default": "delete"
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "The time-to-live in minutes from now until thread should be swept."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "ThreadPatch",
|
"title": "ThreadPatch",
|
||||||
"description": "Payload for updating a thread."
|
"description": "Payload for creating a thread."
|
||||||
},
|
},
|
||||||
"ThreadStateCheckpointRequest": {
|
"ThreadStateCheckpointRequest": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -483,19 +483,19 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
|||||||
|
|
||||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
|
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
|
||||||
|
|
||||||
ADD ./graphs /deps/outer-graphs/src
|
ADD ./graphs /deps/__outer_graphs/src
|
||||||
RUN set -ex && \
|
RUN set -ex && \
|
||||||
for line in '[project]' \
|
for line in '[project]' \
|
||||||
'name = "graphs"' \
|
'name = "graphs"' \
|
||||||
'version = "0.1"' \
|
'version = "0.1"' \
|
||||||
'[tool.setuptools.package-data]' \
|
'[tool.setuptools.package-data]' \
|
||||||
'"*" = ["**/*"]'; do \
|
'"*" = ["**/*"]'; do \
|
||||||
echo "$line" >> /deps/outer-graphs/pyproject.toml; \
|
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
|
||||||
done
|
done
|
||||||
|
|
||||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
|
|
||||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-graphs/src/agent.py:graph", "storm": "/deps/outer-graphs/src/storm.py:graph"}'
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
???+ note "Updating your langgraph.json file"
|
???+ note "Updating your langgraph.json file"
|
||||||
|
|||||||
@@ -22,15 +22,11 @@ To leverage durable execution in LangGraph, you need to:
|
|||||||
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
||||||
|
|
||||||
:::python
|
:::python
|
||||||
|
|
||||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
|
|
||||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Determinism and Consistent Replay
|
## Determinism and Consistent Replay
|
||||||
@@ -65,7 +61,7 @@ LangGraph supports three durability modes that allow you to balance performance
|
|||||||
|
|
||||||
A higher durability mode add more overhead to the workflow execution.
|
A higher durability mode add more overhead to the workflow execution.
|
||||||
|
|
||||||
!!! version-added "Added in version 0.6.0"
|
!!! version-added "Added in v0.6.0"
|
||||||
|
|
||||||
Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management:
|
Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management:
|
||||||
|
|
||||||
@@ -77,16 +73,14 @@ A higher durability mode add more overhead to the workflow execution.
|
|||||||
* `checkpoint_during=True` -> `durability="async"`
|
* `checkpoint_during=True` -> `durability="async"`
|
||||||
* `checkpoint_during=False` -> `durability="exit"`
|
* `checkpoint_during=False` -> `durability="exit"`
|
||||||
|
|
||||||
### `"exit"`
|
|
||||||
|
|
||||||
|
### `"exit"`
|
||||||
Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution.
|
Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution.
|
||||||
|
|
||||||
### `"async"`
|
### `"async"`
|
||||||
|
|
||||||
Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution.
|
Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution.
|
||||||
|
|
||||||
### `"sync"`
|
### `"sync"`
|
||||||
|
|
||||||
Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead.
|
Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead.
|
||||||
|
|
||||||
You can specify the durability mode when calling any graph execution method:
|
You can specify the durability mode when calling any graph execution method:
|
||||||
@@ -316,14 +310,12 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
|||||||
|
|
||||||
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
|
|
||||||
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Starting Points for Resuming Workflows
|
## Starting Points for Resuming Workflows
|
||||||
@@ -334,7 +326,6 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
|||||||
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||||
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
@@ -343,5 +334,4 @@ Once you have enabled durable execution in your workflow, you can resume executi
|
|||||||
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||||
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -1040,7 +1040,7 @@ def node_a(state: State, runtime: Runtime[ContextSchema]):
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full breakdown on configuration.
|
See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::js
|
:::js
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def update_instructions(state: State, store: BaseStore):
|
|||||||
namespace = ("instructions",)
|
namespace = ("instructions",)
|
||||||
current_instructions = store.search(namespace)[0]
|
current_instructions = store.search(namespace)[0]
|
||||||
# Memory logic
|
# Memory logic
|
||||||
prompt = prompt_template.format(instructions=current_instructions.value["instructions"], conversation=state["messages"])
|
prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
|
||||||
output = llm.invoke(prompt)
|
output = llm.invoke(prompt)
|
||||||
new_instructions = output['new_instructions']
|
new_instructions = output['new_instructions']
|
||||||
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
|
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
|
||||||
|
|||||||
@@ -897,5 +897,5 @@ There are two high-level approaches to achieve that:
|
|||||||
|
|
||||||
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
|
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
|
||||||
|
|
||||||
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.md#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
||||||
- Define agent node functions with a [private input state schema](../how-tos/graph-api.md#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
- Define agent node functions with a [private input state schema](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||||
|
|||||||
@@ -1019,7 +1019,7 @@ console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "b" } }));
|
|||||||
# Usage
|
# Usage
|
||||||
input_message = {"role": "user", "content": "hi"}
|
input_message = {"role": "user", "content": "hi"}
|
||||||
# With no configuration, uses default (Anthropic)
|
# With no configuration, uses default (Anthropic)
|
||||||
response_1 = graph.invoke({"messages": [input_message]}, context=ContextSchema())["messages"][-1]
|
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
|
||||||
# Or, can set OpenAI
|
# Or, can set OpenAI
|
||||||
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
|
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
|
||||||
|
|
||||||
@@ -1205,7 +1205,7 @@ There are many use cases where you may wish for your node to have a custom retry
|
|||||||
To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node:
|
To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from langgraph.types import RetryPolicy
|
from langgraph.pregel import RetryPolicy
|
||||||
|
|
||||||
builder.add_node(
|
builder.add_node(
|
||||||
"node_name",
|
"node_name",
|
||||||
@@ -1260,7 +1260,7 @@ By default, the retry policy retries on any exception except for the following:
|
|||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
from langchain.chat_models import init_chat_model
|
from langchain.chat_models import init_chat_model
|
||||||
from langgraph.graph import END, MessagesState, StateGraph, START
|
from langgraph.graph import END, MessagesState, StateGraph, START
|
||||||
from langgraph.types import RetryPolicy
|
from langgraph.pregel import RetryPolicy
|
||||||
from langchain_community.utilities import SQLDatabase
|
from langchain_community.utilities import SQLDatabase
|
||||||
from langchain_core.messages import AIMessage
|
from langchain_core.messages import AIMessage
|
||||||
|
|
||||||
@@ -2110,6 +2110,7 @@ builder.add_edge(START, "generate_topics")
|
|||||||
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
|
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
|
||||||
builder.add_edge("generate_joke", "best_joke")
|
builder.add_edge("generate_joke", "best_joke")
|
||||||
builder.add_edge("best_joke", END)
|
builder.add_edge("best_joke", END)
|
||||||
|
builder.add_edge("generate_topics", END)
|
||||||
graph = builder.compile()
|
graph = builder.compile()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2332,7 +2333,7 @@ from IPython.display import Image, display
|
|||||||
display(Image(graph.get_graph().draw_mermaid_png()))
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
||||||
```
|
```
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
**Using Mermaid + Pyppeteer**
|
**Using Mermaid + Pyppeteer**
|
||||||
|
|
||||||
|
|||||||
@@ -366,8 +366,8 @@ result = graph.invoke(
|
|||||||
|
|
||||||
# Resume with mapping of interrupt IDs to values
|
# Resume with mapping of interrupt IDs to values
|
||||||
resume_map = {
|
resume_map = {
|
||||||
i.id: f"edited text for {i.value['text_to_revise']}"
|
i.interrupt_id: f"human input for prompt {i.value}"
|
||||||
for i in graph.get_state(config).interrupts
|
for i in parent.get_state(thread_config).interrupts
|
||||||
}
|
}
|
||||||
print(graph.invoke(Command(resume=resume_map), config=config))
|
print(graph.invoke(Command(resume=resume_map), config=config))
|
||||||
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
|
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ output = agent.invoke(
|
|||||||
print(output["messages"][-1].text())
|
print(output["messages"][-1].text())
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! version-added "Added in version 0.6.0"
|
!!! version-added "New in langgraph>=0.6"
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ The server will start and open the studio in your browser:
|
|||||||
> - 📚 API Docs: http://127.0.0.1:2024/docs
|
> - 📚 API Docs: http://127.0.0.1:2024/docs
|
||||||
>
|
>
|
||||||
> This in-memory server is designed for development and testing.
|
> This in-memory server is designed for development and testing.
|
||||||
> For production use, please use LangSmith Deployment.
|
> For production use, please use LangGraph Platform.
|
||||||
```
|
```
|
||||||
|
|
||||||
If you were to self-host this on the public internet, anyone could access it!
|
If you were to self-host this on the public internet, anyone could access it!
|
||||||
|
|||||||
@@ -1948,7 +1948,7 @@ const llmWithTools = llm.bindTools(tools);
|
|||||||
|
|
||||||
|
|
||||||
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
|
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
|
||||||
def should_continue(state: MessagesState) -> Literal["Action", END]:
|
def should_continue(state: MessagesState) -> Literal["environment", END]:
|
||||||
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
|
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
|
||||||
|
|
||||||
messages = state["messages"]
|
messages = state["messages"]
|
||||||
|
|||||||
+97
-61
@@ -149,67 +149,6 @@ plugins:
|
|||||||
- tutorials/auth/add_auth_server.md
|
- tutorials/auth/add_auth_server.md
|
||||||
- tutorials/auth/getting_started.md
|
- tutorials/auth/getting_started.md
|
||||||
- tutorials/auth/resource_auth.md
|
- tutorials/auth/resource_auth.md
|
||||||
- agents/agents.md
|
|
||||||
- concepts/why-langgraph.md
|
|
||||||
- tutorials/get-started/1-build-basic-chatbot.md
|
|
||||||
- tutorials/get-started/2-add-tools.md
|
|
||||||
- tutorials/get-started/3-add-memory.md
|
|
||||||
- tutorials/get-started/4-human-in-the-loop.md
|
|
||||||
- tutorials/get-started/5-customize-state.md
|
|
||||||
- tutorials/get-started/6-time-travel.md
|
|
||||||
- tutorials/langgraph-platform/local-server.md
|
|
||||||
- tutorials/workflows.md
|
|
||||||
- concepts/agentic_concepts.md
|
|
||||||
- guides/index.md
|
|
||||||
- agents/overview.md
|
|
||||||
- agents/run_agents.md
|
|
||||||
- concepts/low_level.md
|
|
||||||
- how-tos/graph-api.md
|
|
||||||
- concepts/functional_api.md
|
|
||||||
- how-tos/use-functional-api.md
|
|
||||||
- concepts/pregel.md
|
|
||||||
- concepts/streaming.md
|
|
||||||
- how-tos/streaming.md
|
|
||||||
- concepts/persistence.md
|
|
||||||
- concepts/durable_execution.md
|
|
||||||
- concepts/memory.md
|
|
||||||
- how-tos/memory/add-memory.md
|
|
||||||
- agents/context.md
|
|
||||||
- agents/models.md
|
|
||||||
- concepts/tools.md
|
|
||||||
- how-tos/tool-calling.md
|
|
||||||
- concepts/human_in_the_loop.md
|
|
||||||
- how-tos/human_in_the_loop/add-human-in-the-loop.md
|
|
||||||
- concepts/time-travel.md
|
|
||||||
- how-tos/human_in_the_loop/time-travel.md
|
|
||||||
- concepts/subgraphs.md
|
|
||||||
- how-tos/subgraph.md
|
|
||||||
- concepts/multi_agent.md
|
|
||||||
- agents/multi-agent.md
|
|
||||||
- how-tos/multi_agent.md
|
|
||||||
- concepts/mcp.md
|
|
||||||
- agents/mcp.md
|
|
||||||
- concepts/tracing.md
|
|
||||||
- how-tos/enable-tracing.md
|
|
||||||
- agents/evals.md
|
|
||||||
- examples/index.md
|
|
||||||
- concepts/template_applications.md # TODO: make tutorial
|
|
||||||
- tutorials/rag/langgraph_agentic_rag.md
|
|
||||||
- tutorials/multi_agent/agent_supervisor.md
|
|
||||||
- tutorials/sql/sql-agent.md
|
|
||||||
- agents/ui.md
|
|
||||||
- how-tos/run-id-langsmith.md
|
|
||||||
- troubleshooting/errors/index.md
|
|
||||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
|
||||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
|
||||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
|
||||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
|
||||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
|
||||||
- troubleshooting/errors/INVALID_LICENSE.md
|
|
||||||
- adopters.md
|
|
||||||
- concepts/faq.md
|
|
||||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
|
||||||
|
|
||||||
- tags
|
- tags
|
||||||
- include-markdown
|
- include-markdown
|
||||||
- mkdocstrings:
|
- mkdocstrings:
|
||||||
@@ -247,6 +186,75 @@ plugins:
|
|||||||
- "!^_"
|
- "!^_"
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
|
- Get started:
|
||||||
|
- index.md
|
||||||
|
- Quickstarts:
|
||||||
|
- Start with a prebuilt agent: agents/agents.md
|
||||||
|
- Build a custom workflow:
|
||||||
|
- concepts/why-langgraph.md
|
||||||
|
- 1. Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
|
||||||
|
- 2. Add tools: tutorials/get-started/2-add-tools.md
|
||||||
|
- 3. Add memory: tutorials/get-started/3-add-memory.md
|
||||||
|
- 4. Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
|
||||||
|
- 5. Customize state: tutorials/get-started/5-customize-state.md
|
||||||
|
- 6. Time travel: tutorials/get-started/6-time-travel.md
|
||||||
|
- Run a local server: tutorials/langgraph-platform/local-server.md
|
||||||
|
- General concepts:
|
||||||
|
- Workflows & agents: tutorials/workflows.md
|
||||||
|
- Agent architectures: concepts/agentic_concepts.md
|
||||||
|
|
||||||
|
- Guides:
|
||||||
|
- guides/index.md
|
||||||
|
- Agent development:
|
||||||
|
- Overview: agents/overview.md
|
||||||
|
- Run an agent: agents/run_agents.md
|
||||||
|
- LangGraph APIs:
|
||||||
|
- Graph API:
|
||||||
|
- Overview: concepts/low_level.md
|
||||||
|
- Use the Graph API: how-tos/graph-api.md
|
||||||
|
- Functional API:
|
||||||
|
- Overview: concepts/functional_api.md
|
||||||
|
- Use the Functional API: how-tos/use-functional-api.md
|
||||||
|
- Runtime: concepts/pregel.md
|
||||||
|
- Core capabilities:
|
||||||
|
- Streaming:
|
||||||
|
- Overview: concepts/streaming.md
|
||||||
|
- Stream outputs: how-tos/streaming.md
|
||||||
|
- Persistence:
|
||||||
|
- Overview: concepts/persistence.md
|
||||||
|
- Durable execution:
|
||||||
|
- Overview: concepts/durable_execution.md
|
||||||
|
- Memory:
|
||||||
|
- Overview: concepts/memory.md
|
||||||
|
- Add memory: how-tos/memory/add-memory.md
|
||||||
|
- Context:
|
||||||
|
- Add context: agents/context.md
|
||||||
|
- Models:
|
||||||
|
- Configure model: agents/models.md
|
||||||
|
- Tools:
|
||||||
|
- Overview: concepts/tools.md
|
||||||
|
- Call tools: how-tos/tool-calling.md
|
||||||
|
- Human-in-the-loop:
|
||||||
|
- Overview: concepts/human_in_the_loop.md
|
||||||
|
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
|
||||||
|
- Time travel:
|
||||||
|
- Overview: concepts/time-travel.md
|
||||||
|
- Use time travel: how-tos/human_in_the_loop/time-travel.md
|
||||||
|
- Subgraphs:
|
||||||
|
- Overview: concepts/subgraphs.md
|
||||||
|
- Use subgraphs: how-tos/subgraph.md
|
||||||
|
- Multi-agent:
|
||||||
|
- Overview: concepts/multi_agent.md
|
||||||
|
- Prebuilt implementation: agents/multi-agent.md
|
||||||
|
- Custom implementation: how-tos/multi_agent.md
|
||||||
|
- MCP:
|
||||||
|
- Overview: concepts/mcp.md
|
||||||
|
- Use MCP: agents/mcp.md
|
||||||
|
- Tracing:
|
||||||
|
- Overview: concepts/tracing.md
|
||||||
|
- Enable tracing: how-tos/enable-tracing.md
|
||||||
|
- Evaluate performance: agents/evals.md
|
||||||
|
|
||||||
- Reference:
|
- Reference:
|
||||||
- reference/index.md
|
- reference/index.md
|
||||||
- LangGraph:
|
- LangGraph:
|
||||||
@@ -272,6 +280,34 @@ nav:
|
|||||||
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
|
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
|
||||||
- RemoteGraph: reference/remote_graph.md
|
- RemoteGraph: reference/remote_graph.md
|
||||||
|
|
||||||
|
- Examples:
|
||||||
|
- examples/index.md
|
||||||
|
- Template applications: concepts/template_applications.md # TODO: make tutorial
|
||||||
|
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.md
|
||||||
|
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.md
|
||||||
|
- SQL agent: tutorials/sql/sql-agent.md
|
||||||
|
- Prebuilt chat UI: agents/ui.md
|
||||||
|
- Graph runs in LangSmith: how-tos/run-id-langsmith.md
|
||||||
|
|
||||||
|
- Additional resources:
|
||||||
|
- additional-resources/index.md
|
||||||
|
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||||
|
- LangGraph Academy course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||||
|
- Case studies: adopters.md
|
||||||
|
- concepts/faq.md
|
||||||
|
- llms.txt: llms-txt-overview.md
|
||||||
|
- LangChain Forum: https://forum.langchain.com/
|
||||||
|
- Troubleshooting:
|
||||||
|
- Errors:
|
||||||
|
- troubleshooting/errors/index.md
|
||||||
|
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||||
|
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||||
|
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||||
|
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||||
|
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||||
|
- troubleshooting/errors/INVALID_LICENSE.md
|
||||||
|
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- abbr
|
- abbr
|
||||||
- admonition
|
- admonition
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-banner {
|
.md-banner {
|
||||||
background-color: #FFAE42;
|
background-color: #CFC9FA;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block announce %}
|
{% block announce %}
|
||||||
These docs will be deprecated and removed with the release of LangGraph v1.0 in October 2025. <a href="https://docs.langchain.com/oss/python/langgraph/overview" target="_blank">Visit the v1.0 alpha docs</a>
|
Our new LangChain Academy Course Deep Research with LangGraph is now live! <a href="https://academy.langchain.com/courses/deep-research-with-langgraph/?utm_medium=internal&utm_source=docs&utm_campaign=q3-2025_deep-research-course_co" target="_blank">Enroll for free</a>.
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+4
-4
@@ -7,14 +7,14 @@ name = "langgraph-docs"
|
|||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
description = "LangGraph docs"
|
description = "LangGraph docs"
|
||||||
authors = []
|
authors = []
|
||||||
requires-python = ">=3.11.0,<4.0.0"
|
requires-python = "~=3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohappyeyeballs==2.4.3",
|
"aiohappyeyeballs==2.4.3",
|
||||||
"hub>=3.0.1,<4.0.0",
|
"hub>=3.0.1,<4",
|
||||||
"xxhash>=3.5.0,<4.0.0",
|
"xxhash>=3.5.0,<4",
|
||||||
"black>=25.1.0,<26.0.0",
|
"black>=25.1.0,<26",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
Generated
+4
-5
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.11, <4"
|
requires-python = ">=3.11, <4"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
|
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
|
||||||
@@ -2337,7 +2337,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph"
|
name = "langgraph"
|
||||||
version = "0.6.7"
|
version = "0.6.2"
|
||||||
source = { editable = "../libs/langgraph" }
|
source = { editable = "../libs/langgraph" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "langchain-core" },
|
{ name = "langchain-core" },
|
||||||
@@ -2380,7 +2380,6 @@ dev = [
|
|||||||
{ name = "pytest-repeat" },
|
{ name = "pytest-repeat" },
|
||||||
{ name = "pytest-watcher" },
|
{ name = "pytest-watcher" },
|
||||||
{ name = "pytest-xdist", extras = ["psutil"] },
|
{ name = "pytest-xdist", extras = ["psutil"] },
|
||||||
{ name = "redis" },
|
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "syrupy" },
|
{ name = "syrupy" },
|
||||||
{ name = "types-requests" },
|
{ name = "types-requests" },
|
||||||
@@ -2414,7 +2413,6 @@ dev = [
|
|||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
{ name = "pytest-watcher" },
|
{ name = "pytest-watcher" },
|
||||||
{ name = "redis" },
|
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2645,7 +2643,7 @@ test = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-prebuilt"
|
name = "langgraph-prebuilt"
|
||||||
version = "0.6.4"
|
version = "0.6.2"
|
||||||
source = { editable = "../libs/prebuilt" }
|
source = { editable = "../libs/prebuilt" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "langchain-core" },
|
{ name = "langchain-core" },
|
||||||
@@ -2676,6 +2674,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-sdk"
|
name = "langgraph-sdk"
|
||||||
|
version = "0.2.0"
|
||||||
source = { editable = "../libs/sdk-py" }
|
source = { editable = "../libs/sdk-py" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"id": "18526f23",
|
"id": "18526f23",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md"
|
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -707,9 +707,7 @@
|
|||||||
" \"\"\"\n",
|
" \"\"\"\n",
|
||||||
" Find all tool calls in the messages returned\n",
|
" Find all tool calls in the messages returned\n",
|
||||||
" \"\"\"\n",
|
" \"\"\"\n",
|
||||||
" tool_calls = [\n",
|
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
|
||||||
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
|
|
||||||
" ]\n",
|
|
||||||
" return tool_calls\n",
|
" return tool_calls\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 LangChain, Inc.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -7,6 +7,11 @@ from contextlib import contextmanager
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||||
|
from psycopg.rows import DictRow, dict_row
|
||||||
|
from psycopg.types.json import Jsonb
|
||||||
|
from psycopg_pool import ConnectionPool
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
ChannelVersions,
|
ChannelVersions,
|
||||||
@@ -14,17 +19,12 @@ from langgraph.checkpoint.base import (
|
|||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
CheckpointTuple,
|
CheckpointTuple,
|
||||||
get_checkpoint_id,
|
get_checkpoint_id,
|
||||||
get_serializable_checkpoint_metadata,
|
get_checkpoint_metadata,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
||||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
||||||
from psycopg.rows import DictRow, dict_row
|
|
||||||
from psycopg.types.json import Jsonb
|
|
||||||
from psycopg_pool import ConnectionPool
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres import _internal
|
from langgraph.checkpoint.postgres import _internal
|
||||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||||
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
||||||
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||||
|
|
||||||
Conn = _internal.Conn # For backward compatibility
|
Conn = _internal.Conn # For backward compatibility
|
||||||
|
|
||||||
@@ -94,10 +94,9 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
for v, migration in zip(
|
for v, migration in zip(
|
||||||
range(version + 1, len(self.MIGRATIONS)),
|
range(version + 1, len(self.MIGRATIONS)),
|
||||||
self.MIGRATIONS[version + 1 :],
|
self.MIGRATIONS[version + 1 :],
|
||||||
strict=False,
|
|
||||||
):
|
):
|
||||||
cur.execute(migration)
|
cur.execute(migration)
|
||||||
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||||
if self.pipe:
|
if self.pipe:
|
||||||
self.pipe.sync()
|
self.pipe.sync()
|
||||||
|
|
||||||
@@ -116,12 +115,12 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: The config to use for listing the checkpoints.
|
config: The config to use for listing the checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata. Defaults to None.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: The maximum number of checkpoints to return.
|
limit: The maximum number of checkpoints to return. Defaults to None.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An iterator of checkpoint tuples.
|
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
>>> from langgraph.checkpoint.postgres import PostgresSaver
|
||||||
@@ -183,7 +182,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -191,7 +190,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
@@ -326,7 +325,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
checkpoint["id"],
|
checkpoint["id"],
|
||||||
checkpoint_id,
|
checkpoint_id,
|
||||||
Jsonb(copy),
|
Jsonb(copy),
|
||||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return next_config
|
return next_config
|
||||||
@@ -451,7 +450,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
{
|
{
|
||||||
**value["checkpoint"],
|
**value["checkpoint"],
|
||||||
"channel_values": {
|
"channel_values": {
|
||||||
**(value["checkpoint"].get("channel_values") or {}),
|
**value["checkpoint"].get("channel_values"),
|
||||||
**self._load_blobs(value["channel_values"]),
|
**self._load_blobs(value["channel_values"]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
from psycopg.rows import DictRow
|
from psycopg.rows import DictRow
|
||||||
from psycopg_pool import AsyncConnectionPool
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
Conn = AsyncConnection[DictRow] | AsyncConnectionPool[AsyncConnection[DictRow]]
|
Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from psycopg import Connection
|
from psycopg import Connection
|
||||||
from psycopg.rows import DictRow
|
from psycopg.rows import DictRow
|
||||||
from psycopg_pool import ConnectionPool
|
from psycopg_pool import ConnectionPool
|
||||||
|
|
||||||
Conn = Connection[DictRow] | ConnectionPool[Connection[DictRow]]
|
Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||||
|
from psycopg.rows import DictRow, dict_row
|
||||||
|
from psycopg.types.json import Jsonb
|
||||||
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
ChannelVersions,
|
ChannelVersions,
|
||||||
@@ -14,17 +19,12 @@ from langgraph.checkpoint.base import (
|
|||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
CheckpointTuple,
|
CheckpointTuple,
|
||||||
get_checkpoint_id,
|
get_checkpoint_id,
|
||||||
get_serializable_checkpoint_metadata,
|
get_checkpoint_metadata,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
||||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
||||||
from psycopg.rows import DictRow, dict_row
|
|
||||||
from psycopg.types.json import Jsonb
|
|
||||||
from psycopg_pool import AsyncConnectionPool
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres import _ainternal
|
from langgraph.checkpoint.postgres import _ainternal
|
||||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||||
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
||||||
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||||
|
|
||||||
Conn = _ainternal.Conn # For backward compatibility
|
Conn = _ainternal.Conn # For backward compatibility
|
||||||
|
|
||||||
@@ -99,12 +99,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
for v, migration in zip(
|
for v, migration in zip(
|
||||||
range(version + 1, len(self.MIGRATIONS)),
|
range(version + 1, len(self.MIGRATIONS)),
|
||||||
self.MIGRATIONS[version + 1 :],
|
self.MIGRATIONS[version + 1 :],
|
||||||
strict=False,
|
|
||||||
):
|
):
|
||||||
await cur.execute(migration)
|
await cur.execute(migration)
|
||||||
await cur.execute(
|
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||||
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
|
||||||
)
|
|
||||||
if self.pipe:
|
if self.pipe:
|
||||||
await self.pipe.sync()
|
await self.pipe.sync()
|
||||||
|
|
||||||
@@ -124,11 +121,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
Args:
|
Args:
|
||||||
config: Base configuration for filtering checkpoints.
|
config: Base configuration for filtering checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An asynchronous iterator of matching checkpoint tuples.
|
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
where, args = self._search_where(config, filter, before)
|
where, args = self._search_where(config, filter, before)
|
||||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||||
@@ -172,7 +169,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -180,7 +177,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
thread_id = config["configurable"]["thread_id"]
|
thread_id = config["configurable"]["thread_id"]
|
||||||
checkpoint_id = get_checkpoint_id(config)
|
checkpoint_id = get_checkpoint_id(config)
|
||||||
@@ -286,7 +283,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
checkpoint["id"],
|
checkpoint["id"],
|
||||||
checkpoint_id,
|
checkpoint_id,
|
||||||
Jsonb(copy),
|
Jsonb(copy),
|
||||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return next_config
|
return next_config
|
||||||
@@ -412,7 +409,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
{
|
{
|
||||||
**value["checkpoint"],
|
**value["checkpoint"],
|
||||||
"channel_values": {
|
"channel_values": {
|
||||||
**(value["checkpoint"].get("channel_values") or {}),
|
**value["checkpoint"].get("channel_values"),
|
||||||
**self._load_blobs(value["channel_values"]),
|
**self._load_blobs(value["channel_values"]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -447,11 +444,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
Args:
|
Args:
|
||||||
config: Base configuration for filtering checkpoints.
|
config: Base configuration for filtering checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An iterator of matching checkpoint tuples.
|
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# check if we are in the main thread, only bg threads can block
|
# check if we are in the main thread, only bg threads can block
|
||||||
@@ -479,7 +476,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -487,7 +484,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# check if we are in the main thread, only bg threads can block
|
# check if we are in the main thread, only bg threads can block
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import warnings
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from importlib.metadata import version as get_version
|
from typing import Any, Optional, cast
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from psycopg.types.json import Jsonb
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
BaseCheckpointSaver,
|
BaseCheckpointSaver,
|
||||||
@@ -14,21 +14,8 @@ from langgraph.checkpoint.base import (
|
|||||||
get_checkpoint_id,
|
get_checkpoint_id,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.types import TASKS
|
from langgraph.checkpoint.serde.types import TASKS
|
||||||
from psycopg.types.json import Jsonb
|
|
||||||
|
|
||||||
MetadataInput = dict[str, Any] | None
|
MetadataInput = Optional[dict[str, Any]]
|
||||||
|
|
||||||
try:
|
|
||||||
major, minor = get_version("langgraph").split(".")[:2]
|
|
||||||
if int(major) == 0 and int(minor) < 5:
|
|
||||||
warnings.warn(
|
|
||||||
"You're using incompatible versions of langgraph and checkpoint-postgres. Please upgrade langgraph to avoid unexpected behavior.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# skip version check if running from source
|
|
||||||
pass
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
To add a new migration, add a new string to the MIGRATIONS list.
|
To add a new migration, add a new string to the MIGRATIONS list.
|
||||||
@@ -81,7 +68,7 @@ MIGRATIONS = [
|
|||||||
"""
|
"""
|
||||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
||||||
""",
|
""",
|
||||||
"""ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
|
"""ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
|
||||||
]
|
]
|
||||||
|
|
||||||
SELECT_SQL = """
|
SELECT_SQL = """
|
||||||
|
|||||||
@@ -3,19 +3,9 @@ import threading
|
|||||||
import warnings
|
import warnings
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import asynccontextmanager, contextmanager
|
from contextlib import asynccontextmanager, contextmanager
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from langgraph.checkpoint.base import (
|
|
||||||
WRITES_IDX_MAP,
|
|
||||||
ChannelVersions,
|
|
||||||
Checkpoint,
|
|
||||||
CheckpointMetadata,
|
|
||||||
CheckpointTuple,
|
|
||||||
get_serializable_checkpoint_metadata,
|
|
||||||
)
|
|
||||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
||||||
from langgraph.checkpoint.serde.types import TASKS
|
|
||||||
from psycopg import (
|
from psycopg import (
|
||||||
AsyncConnection,
|
AsyncConnection,
|
||||||
AsyncCursor,
|
AsyncCursor,
|
||||||
@@ -29,8 +19,18 @@ from psycopg.rows import DictRow, dict_row
|
|||||||
from psycopg.types.json import Jsonb
|
from psycopg.types.json import Jsonb
|
||||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||||
|
|
||||||
|
from langgraph.checkpoint.base import (
|
||||||
|
WRITES_IDX_MAP,
|
||||||
|
ChannelVersions,
|
||||||
|
Checkpoint,
|
||||||
|
CheckpointMetadata,
|
||||||
|
CheckpointTuple,
|
||||||
|
get_checkpoint_metadata,
|
||||||
|
)
|
||||||
from langgraph.checkpoint.postgres import _ainternal, _internal
|
from langgraph.checkpoint.postgres import _ainternal, _internal
|
||||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||||
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||||
|
from langgraph.checkpoint.serde.types import TASKS
|
||||||
|
|
||||||
"""
|
"""
|
||||||
To add a new migration, add a new string to the MIGRATIONS list.
|
To add a new migration, add a new string to the MIGRATIONS list.
|
||||||
@@ -77,7 +77,7 @@ MIGRATIONS = [
|
|||||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
||||||
""",
|
""",
|
||||||
"""
|
"""
|
||||||
ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
|
ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ def _dump_blobs(
|
|||||||
checkpoint_ns: str,
|
checkpoint_ns: str,
|
||||||
values: dict[str, Any],
|
values: dict[str, Any],
|
||||||
versions: ChannelVersions,
|
versions: ChannelVersions,
|
||||||
) -> list[tuple[str, str, str, str, bytes | None]]:
|
) -> list[tuple[str, str, str, str, Optional[bytes]]]:
|
||||||
if not versions:
|
if not versions:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -186,8 +186,8 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conn: _internal.Conn,
|
conn: _internal.Conn,
|
||||||
pipe: Pipeline | None = None,
|
pipe: Optional[Pipeline] = None,
|
||||||
serde: SerializerProtocol | None = None,
|
serde: Optional[SerializerProtocol] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||||
@@ -249,20 +249,19 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|||||||
for v, migration in zip(
|
for v, migration in zip(
|
||||||
range(version + 1, len(self.MIGRATIONS)),
|
range(version + 1, len(self.MIGRATIONS)),
|
||||||
self.MIGRATIONS[version + 1 :],
|
self.MIGRATIONS[version + 1 :],
|
||||||
strict=False,
|
|
||||||
):
|
):
|
||||||
cur.execute(migration)
|
cur.execute(migration)
|
||||||
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||||
if self.pipe:
|
if self.pipe:
|
||||||
self.pipe.sync()
|
self.pipe.sync()
|
||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig | None,
|
config: Optional[RunnableConfig],
|
||||||
*,
|
*,
|
||||||
filter: dict[str, Any] | None = None,
|
filter: Optional[dict[str, Any]] = None,
|
||||||
before: RunnableConfig | None = None,
|
before: Optional[RunnableConfig] = None,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database.
|
"""List checkpoints from the database.
|
||||||
|
|
||||||
@@ -300,7 +299,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|||||||
pending_writes=self._load_writes(value["pending_writes"]),
|
pending_writes=self._load_writes(value["pending_writes"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
@@ -310,7 +309,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
@@ -442,7 +441,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|||||||
thread_id,
|
thread_id,
|
||||||
checkpoint_ns,
|
checkpoint_ns,
|
||||||
Jsonb(copy),
|
Jsonb(copy),
|
||||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return next_config
|
return next_config
|
||||||
@@ -543,8 +542,8 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conn: _ainternal.Conn,
|
conn: _ainternal.Conn,
|
||||||
pipe: AsyncPipeline | None = None,
|
pipe: Optional[AsyncPipeline] = None,
|
||||||
serde: SerializerProtocol | None = None,
|
serde: Optional[SerializerProtocol] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||||
@@ -571,7 +570,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
pipeline: bool = False,
|
pipeline: bool = False,
|
||||||
serde: SerializerProtocol | None = None,
|
serde: Optional[SerializerProtocol] = None,
|
||||||
) -> AsyncIterator["AsyncShallowPostgresSaver"]:
|
) -> AsyncIterator["AsyncShallowPostgresSaver"]:
|
||||||
"""Create a new AsyncShallowPostgresSaver instance from a connection string.
|
"""Create a new AsyncShallowPostgresSaver instance from a connection string.
|
||||||
|
|
||||||
@@ -611,22 +610,19 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
for v, migration in zip(
|
for v, migration in zip(
|
||||||
range(version + 1, len(self.MIGRATIONS)),
|
range(version + 1, len(self.MIGRATIONS)),
|
||||||
self.MIGRATIONS[version + 1 :],
|
self.MIGRATIONS[version + 1 :],
|
||||||
strict=False,
|
|
||||||
):
|
):
|
||||||
await cur.execute(migration)
|
await cur.execute(migration)
|
||||||
await cur.execute(
|
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||||
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
|
||||||
)
|
|
||||||
if self.pipe:
|
if self.pipe:
|
||||||
await self.pipe.sync()
|
await self.pipe.sync()
|
||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig | None,
|
config: Optional[RunnableConfig],
|
||||||
*,
|
*,
|
||||||
filter: dict[str, Any] | None = None,
|
filter: Optional[dict[str, Any]] = None,
|
||||||
before: RunnableConfig | None = None,
|
before: Optional[RunnableConfig] = None,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database asynchronously.
|
"""List checkpoints from the database asynchronously.
|
||||||
|
|
||||||
@@ -666,7 +662,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
@@ -676,7 +672,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
thread_id = config["configurable"]["thread_id"]
|
thread_id = config["configurable"]["thread_id"]
|
||||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||||
@@ -778,7 +774,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
thread_id,
|
thread_id,
|
||||||
checkpoint_ns,
|
checkpoint_ns,
|
||||||
Jsonb(copy),
|
Jsonb(copy),
|
||||||
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
|
Jsonb(get_checkpoint_metadata(config, metadata)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return next_config
|
return next_config
|
||||||
@@ -865,11 +861,11 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig | None,
|
config: Optional[RunnableConfig],
|
||||||
*,
|
*,
|
||||||
filter: dict[str, Any] | None = None,
|
filter: Optional[dict[str, Any]] = None,
|
||||||
before: RunnableConfig | None = None,
|
before: Optional[RunnableConfig] = None,
|
||||||
limit: int | None = None,
|
limit: Optional[int] = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database.
|
"""List checkpoints from the database.
|
||||||
|
|
||||||
@@ -887,7 +883,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
except StopAsyncIteration:
|
except StopAsyncIteration:
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
@@ -897,7 +893,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# check if we are in the main thread, only bg threads can block
|
# check if we are in the main thread, only bg threads can block
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from langgraph.store.postgres.aio import AsyncPostgresStore
|
from langgraph.store.postgres.aio import AsyncPostgresStore
|
||||||
from langgraph.store.postgres.base import PoolConfig, PostgresStore
|
from langgraph.store.postgres.base import PostgresStore
|
||||||
|
|
||||||
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
|
__all__ = ["AsyncPostgresStore", "PostgresStore"]
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||||
|
from psycopg.rows import DictRow, dict_row
|
||||||
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
|
from langgraph.checkpoint.postgres import _ainternal
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
ListNamespacesOp,
|
ListNamespacesOp,
|
||||||
@@ -17,11 +22,6 @@ from langgraph.store.base import (
|
|||||||
SearchOp,
|
SearchOp,
|
||||||
)
|
)
|
||||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
||||||
from psycopg.rows import DictRow, dict_row
|
|
||||||
from psycopg_pool import AsyncConnectionPool
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres import _ainternal
|
|
||||||
from langgraph.store.postgres.base import (
|
from langgraph.store.postgres.base import (
|
||||||
PLACEHOLDER,
|
PLACEHOLDER,
|
||||||
BasePostgresStore,
|
BasePostgresStore,
|
||||||
@@ -339,7 +339,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Maximum time to wait for the task to stop, in seconds.
|
timeout: Maximum time to wait for the task to stop, in seconds.
|
||||||
If `None`, wait indefinitely.
|
If None, wait indefinitely.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the task was successfully stopped or wasn't running,
|
bool: True if the task was successfully stopped or wasn't running,
|
||||||
@@ -465,9 +465,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
query,
|
query,
|
||||||
[
|
[
|
||||||
p
|
p
|
||||||
for (ns, k, pathname, _), vector in zip(
|
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
|
||||||
txt_params, vectors, strict=False
|
|
||||||
)
|
|
||||||
for p in (ns, k, pathname, vector)
|
for p in (ns, k, pathname, vector)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -488,13 +486,13 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
vectors = await self.embeddings.aembed_documents(
|
vectors = await self.embeddings.aembed_documents(
|
||||||
[query for _, query in embedding_requests]
|
[query for _, query in embedding_requests]
|
||||||
)
|
)
|
||||||
for (idx, _), vector in zip(embedding_requests, vectors, strict=False):
|
for (idx, _), vector in zip(embedding_requests, vectors):
|
||||||
_paramslist = queries[idx][1]
|
_paramslist = queries[idx][1]
|
||||||
for i in range(len(_paramslist)):
|
for i in range(len(_paramslist)):
|
||||||
if _paramslist[i] is PLACEHOLDER:
|
if _paramslist[i] is PLACEHOLDER:
|
||||||
_paramslist[i] = vector
|
_paramslist[i] = vector
|
||||||
|
|
||||||
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||||
await cur.execute(query, params)
|
await cur.execute(query, params)
|
||||||
rows = cast(list[Row], await cur.fetchall())
|
rows = cast(list[Row], await cur.fetchall())
|
||||||
items = [
|
items = [
|
||||||
@@ -512,7 +510,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
cur: AsyncCursor[DictRow],
|
cur: AsyncCursor[DictRow],
|
||||||
) -> None:
|
) -> None:
|
||||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||||
await cur.execute(query, params)
|
await cur.execute(query, params)
|
||||||
rows = cast(list[dict], await cur.fetchall())
|
rows = cast(list[dict], await cur.fetchall())
|
||||||
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
||||||
|
|||||||
@@ -6,20 +6,30 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
|
Callable,
|
||||||
Generic,
|
Generic,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||||
|
from psycopg.rows import DictRow, dict_row
|
||||||
|
from psycopg.types.json import Jsonb
|
||||||
|
from psycopg_pool import ConnectionPool
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
||||||
|
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
BaseStore,
|
BaseStore,
|
||||||
GetOp,
|
GetOp,
|
||||||
@@ -36,14 +46,6 @@ from langgraph.store.base import (
|
|||||||
get_text_at_path,
|
get_text_at_path,
|
||||||
tokenize_path,
|
tokenize_path,
|
||||||
)
|
)
|
||||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
||||||
from psycopg.rows import DictRow, dict_row
|
|
||||||
from psycopg.types.json import Jsonb
|
|
||||||
from psycopg_pool import ConnectionPool
|
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
|
||||||
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
@@ -139,7 +141,7 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS store_vectors_embedding_idx ON store_vec
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
C = TypeVar("C", bound=_pg_internal.Conn | _ainternal.Conn)
|
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
|
||||||
|
|
||||||
|
|
||||||
class PoolConfig(TypedDict, total=False):
|
class PoolConfig(TypedDict, total=False):
|
||||||
@@ -253,7 +255,7 @@ class BasePostgresStore(Generic[C]):
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for namespace, items in namespace_groups.items():
|
for namespace, items in namespace_groups.items():
|
||||||
_, keys = zip(*items, strict=False)
|
_, keys = zip(*items)
|
||||||
this_refresh_ttls = refresh_ttls[namespace]
|
this_refresh_ttls = refresh_ttls[namespace]
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
@@ -866,7 +868,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Maximum time to wait for the thread to stop, in seconds.
|
timeout: Maximum time to wait for the thread to stop, in seconds.
|
||||||
If `None`, wait indefinitely.
|
If None, wait indefinitely.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the thread was successfully stopped or wasn't running,
|
bool: True if the thread was successfully stopped or wasn't running,
|
||||||
@@ -1012,9 +1014,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
query,
|
query,
|
||||||
[
|
[
|
||||||
p
|
p
|
||||||
for (ns, k, pathname, _), vector in zip(
|
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
|
||||||
txt_params, vectors, strict=False
|
|
||||||
)
|
|
||||||
for p in (ns, k, pathname, vector)
|
for p in (ns, k, pathname, vector)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -1035,15 +1035,13 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
embeddings = self.embeddings.embed_documents(
|
embeddings = self.embeddings.embed_documents(
|
||||||
[query for _, query in embedding_requests]
|
[query for _, query in embedding_requests]
|
||||||
)
|
)
|
||||||
for (idx, _), embedding in zip(
|
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||||
embedding_requests, embeddings, strict=False
|
|
||||||
):
|
|
||||||
_paramslist = queries[idx][1]
|
_paramslist = queries[idx][1]
|
||||||
for i in range(len(_paramslist)):
|
for i in range(len(_paramslist)):
|
||||||
if _paramslist[i] is PLACEHOLDER:
|
if _paramslist[i] is PLACEHOLDER:
|
||||||
_paramslist[i] = embedding
|
_paramslist[i] = embedding
|
||||||
|
|
||||||
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
rows = cast(list[Row], cur.fetchall())
|
rows = cast(list[Row], cur.fetchall())
|
||||||
results[idx] = [
|
results[idx] = [
|
||||||
@@ -1060,7 +1058,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
cur: Cursor[DictRow],
|
cur: Cursor[DictRow],
|
||||||
) -> None:
|
) -> None:
|
||||||
for (query, params), (idx, _) in zip(
|
for (query, params), (idx, _) in zip(
|
||||||
self._get_batch_list_namespaces_queries(list_ops), list_ops, strict=False
|
self._get_batch_list_namespaces_queries(list_ops), list_ops
|
||||||
):
|
):
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
|
results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
|
||||||
|
|||||||
@@ -4,45 +4,36 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "langgraph-checkpoint-postgres"
|
name = "langgraph-checkpoint-postgres"
|
||||||
version = "3.0.1"
|
version = "2.0.23"
|
||||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||||
authors = []
|
authors = []
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.9"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ['LICENSE']
|
license-files = ['LICENSE']
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"langgraph-checkpoint>=2.1.2,<4.0.0",
|
"langgraph-checkpoint>=2.0.21,<3.0.0",
|
||||||
"orjson>=3.10.1",
|
"orjson>=3.10.1",
|
||||||
"psycopg>=3.2.0",
|
"psycopg>=3.2.0",
|
||||||
"psycopg-pool>=3.2.0",
|
"psycopg-pool>=3.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
|
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||||
Twitter = "https://x.com/LangChainAI"
|
|
||||||
Slack = "https://www.langchain.com/join-community"
|
|
||||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
test = [
|
dev = [
|
||||||
|
"ruff",
|
||||||
|
"codespell",
|
||||||
"pytest",
|
"pytest",
|
||||||
"anyio",
|
"anyio",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
|
"mypy",
|
||||||
"psycopg[binary]",
|
"psycopg[binary]",
|
||||||
"langgraph-checkpoint",
|
"langgraph-checkpoint",
|
||||||
"pytest-watcher",
|
"pytest-watcher",
|
||||||
]
|
]
|
||||||
lint = [
|
|
||||||
"ruff",
|
|
||||||
"codespell",
|
|
||||||
"mypy",
|
|
||||||
]
|
|
||||||
dev = [
|
|
||||||
{include-group = "test"},
|
|
||||||
{include-group = "lint"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
default-groups = ['dev']
|
default-groups = ['dev']
|
||||||
@@ -64,10 +55,8 @@ lint.select = [
|
|||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
"UP", # pyupgrade
|
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008"]
|
lint.ignore = ["E501", "B008"]
|
||||||
target-version = "py310"
|
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ from uuid import uuid4
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from psycopg import AsyncConnection
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
EXCLUDED_METADATA_KEYS,
|
EXCLUDED_METADATA_KEYS,
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
@@ -13,15 +17,11 @@ from langgraph.checkpoint.base import (
|
|||||||
create_checkpoint,
|
create_checkpoint,
|
||||||
empty_checkpoint,
|
empty_checkpoint,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.types import TASKS
|
|
||||||
from psycopg import AsyncConnection
|
|
||||||
from psycopg.rows import dict_row
|
|
||||||
from psycopg_pool import AsyncConnectionPool
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres.aio import (
|
from langgraph.checkpoint.postgres.aio import (
|
||||||
AsyncPostgresSaver,
|
AsyncPostgresSaver,
|
||||||
AsyncShallowPostgresSaver,
|
AsyncShallowPostgresSaver,
|
||||||
)
|
)
|
||||||
|
from langgraph.checkpoint.serde.types import TASKS
|
||||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||||
|
|
||||||
|
|
||||||
@@ -187,11 +187,13 @@ def test_data():
|
|||||||
metadata_1: CheckpointMetadata = {
|
metadata_1: CheckpointMetadata = {
|
||||||
"source": "input",
|
"source": "input",
|
||||||
"step": 2,
|
"step": 2,
|
||||||
|
"writes": {},
|
||||||
"score": 1,
|
"score": 1,
|
||||||
}
|
}
|
||||||
metadata_2: CheckpointMetadata = {
|
metadata_2: CheckpointMetadata = {
|
||||||
"source": "loop",
|
"source": "loop",
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
"score": None,
|
"score": None,
|
||||||
}
|
}
|
||||||
metadata_3: CheckpointMetadata = {}
|
metadata_3: CheckpointMetadata = {}
|
||||||
@@ -218,6 +220,7 @@ async def test_combined_metadata(saver_name: str, test_data) -> None:
|
|||||||
metadata: CheckpointMetadata = {
|
metadata: CheckpointMetadata = {
|
||||||
"source": "loop",
|
"source": "loop",
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
"score": None,
|
"score": None,
|
||||||
}
|
}
|
||||||
await saver.aput(config, chkpnt, metadata, {})
|
await saver.aput(config, chkpnt, metadata, {})
|
||||||
@@ -243,6 +246,7 @@ async def test_asearch(saver_name: str, test_data) -> None:
|
|||||||
query_1 = {"source": "input"} # search by 1 key
|
query_1 = {"source": "input"} # search by 1 key
|
||||||
query_2 = {
|
query_2 = {
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
} # search by multiple keys
|
} # search by multiple keys
|
||||||
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
||||||
query_4 = {"source": "update", "step": 1} # no match
|
query_4 = {"source": "update", "step": 1} # no match
|
||||||
@@ -340,34 +344,3 @@ async def test_pending_sends_migration(saver_name: str) -> None:
|
|||||||
TASKS: ["send-1", "send-2", "send-3"]
|
TASKS: ["send-1", "send-2", "send-3"]
|
||||||
}
|
}
|
||||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
|
||||||
async def test_get_checkpoint_no_channel_values(
|
|
||||||
monkeypatch, saver_name: str, test_data
|
|
||||||
) -> None:
|
|
||||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
|
||||||
async with _saver(saver_name) as saver:
|
|
||||||
config = {
|
|
||||||
"configurable": {
|
|
||||||
"thread_id": "thread-2",
|
|
||||||
"checkpoint_ns": "",
|
|
||||||
"__super_private_key": "super_private_value",
|
|
||||||
},
|
|
||||||
"metadata": {"run_id": "my_run_id"},
|
|
||||||
}
|
|
||||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
|
||||||
await saver.aput(config, chkpnt, {}, {})
|
|
||||||
|
|
||||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
|
||||||
|
|
||||||
def patched_load_checkpoint_tuple(value):
|
|
||||||
value["checkpoint"].pop("channel_values", None)
|
|
||||||
return load_checkpoint_tuple(value)
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
|
||||||
)
|
|
||||||
|
|
||||||
checkpoint = await saver.aget_tuple(config)
|
|
||||||
assert checkpoint.checkpoint["channel_values"] == {}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@@ -11,6 +12,8 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
from psycopg import AsyncConnection
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
Item,
|
Item,
|
||||||
@@ -18,8 +21,6 @@ from langgraph.store.base import (
|
|||||||
PutOp,
|
PutOp,
|
||||||
SearchOp,
|
SearchOp,
|
||||||
)
|
)
|
||||||
from psycopg import AsyncConnection
|
|
||||||
|
|
||||||
from langgraph.store.postgres import AsyncPostgresStore
|
from langgraph.store.postgres import AsyncPostgresStore
|
||||||
from tests.conftest import (
|
from tests.conftest import (
|
||||||
DEFAULT_URI,
|
DEFAULT_URI,
|
||||||
@@ -33,6 +34,9 @@ TTL_MINUTES = TTL_SECONDS / 60
|
|||||||
|
|
||||||
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
|
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
|
||||||
async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||||
|
|
||||||
database = f"test_{uuid.uuid4().hex[:16]}"
|
database = f"test_{uuid.uuid4().hex[:16]}"
|
||||||
uri_parts = DEFAULT_URI.split("/")
|
uri_parts = DEFAULT_URI.split("/")
|
||||||
uri_base = "/".join(uri_parts[:-1])
|
uri_base = "/".join(uri_parts[:-1])
|
||||||
@@ -354,6 +358,8 @@ async def _create_vector_store(
|
|||||||
text_fields: list[str] | None = None,
|
text_fields: list[str] | None = None,
|
||||||
) -> AsyncIterator[AsyncPostgresStore]:
|
) -> AsyncIterator[AsyncPostgresStore]:
|
||||||
"""Create a store with vector search enabled."""
|
"""Create a store with vector search enabled."""
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||||
|
|
||||||
database = f"test_{uuid.uuid4().hex[:16]}"
|
database = f"test_{uuid.uuid4().hex[:16]}"
|
||||||
uri_parts = DEFAULT_URI.split("/")
|
uri_parts = DEFAULT_URI.split("/")
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from uuid import uuid4
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
from psycopg import Connection
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
Item,
|
Item,
|
||||||
@@ -17,8 +19,6 @@ from langgraph.store.base import (
|
|||||||
PutOp,
|
PutOp,
|
||||||
SearchOp,
|
SearchOp,
|
||||||
)
|
)
|
||||||
from psycopg import Connection
|
|
||||||
|
|
||||||
from langgraph.store.postgres import PostgresStore
|
from langgraph.store.postgres import PostgresStore
|
||||||
from tests.conftest import (
|
from tests.conftest import (
|
||||||
DEFAULT_URI,
|
DEFAULT_URI,
|
||||||
@@ -754,7 +754,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
|||||||
|
|
||||||
similarities = []
|
similarities = []
|
||||||
for y in Y:
|
for y in Y:
|
||||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
dot_product = sum(a * b for a, b in zip(X, y))
|
||||||
norm1 = sum(a * a for a in X) ** 0.5
|
norm1 = sum(a * a for a in X) ** 0.5
|
||||||
norm2 = sum(a * a for a in y) ** 0.5
|
norm2 = sum(a * a for a in y) ** 0.5
|
||||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||||
@@ -771,7 +771,7 @@ def _inner_product(X: list[float], Y: list[list[float]]) -> list[float]:
|
|||||||
|
|
||||||
similarities = []
|
similarities = []
|
||||||
for y in Y:
|
for y in Y:
|
||||||
similarity = sum(a * b for a, b in zip(X, y, strict=False))
|
similarity = sum(a * b for a, b in zip(X, y))
|
||||||
similarities.append(similarity)
|
similarities.append(similarity)
|
||||||
|
|
||||||
return similarities
|
return similarities
|
||||||
@@ -785,7 +785,7 @@ def _neg_l2_distance(X: list[float], Y: list[list[float]]) -> list[float]:
|
|||||||
|
|
||||||
similarities = []
|
similarities = []
|
||||||
for y in Y:
|
for y in Y:
|
||||||
similarity = sum((a - b) ** 2 for a, b in zip(X, y, strict=False)) ** 0.5
|
similarity = sum((a - b) ** 2 for a, b in zip(X, y)) ** 0.5
|
||||||
similarities.append(-similarity)
|
similarities.append(-similarity)
|
||||||
|
|
||||||
return similarities
|
return similarities
|
||||||
@@ -861,41 +861,3 @@ def test_store_ttl(store):
|
|||||||
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
|
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
|
||||||
res = store.search(ns, query="bar", refresh_ttl=False)
|
res = store.search(ns, query="bar", refresh_ttl=False)
|
||||||
assert len(res) == 0
|
assert len(res) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"vector_type,distance_type",
|
|
||||||
[
|
|
||||||
("vector", "cosine"),
|
|
||||||
("vector", "inner_product"),
|
|
||||||
("halfvec", "cosine"),
|
|
||||||
("halfvec", "inner_product"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_non_ascii(
|
|
||||||
request: Any,
|
|
||||||
fake_embeddings: CharacterEmbeddings,
|
|
||||||
vector_type: str,
|
|
||||||
distance_type: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test support for non-ascii characters"""
|
|
||||||
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
|
|
||||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
|
||||||
store.put(
|
|
||||||
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
|
||||||
) # Japanese
|
|
||||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
|
||||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
|
||||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
|
||||||
|
|
||||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
|
||||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
|
||||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
|
||||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
|
||||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
|
||||||
|
|
||||||
assert result1[0].key == "1"
|
|
||||||
assert result2[0].key == "2"
|
|
||||||
assert result3[0].key == "3"
|
|
||||||
assert result4[0].key == "4"
|
|
||||||
assert result5[0].key == "5"
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from uuid import uuid4
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from psycopg import Connection
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
from psycopg_pool import ConnectionPool
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
EXCLUDED_METADATA_KEYS,
|
EXCLUDED_METADATA_KEYS,
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
@@ -14,12 +18,8 @@ from langgraph.checkpoint.base import (
|
|||||||
create_checkpoint,
|
create_checkpoint,
|
||||||
empty_checkpoint,
|
empty_checkpoint,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.types import TASKS
|
|
||||||
from psycopg import Connection
|
|
||||||
from psycopg.rows import dict_row
|
|
||||||
from psycopg_pool import ConnectionPool
|
|
||||||
|
|
||||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||||
|
from langgraph.checkpoint.serde.types import TASKS
|
||||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||||
|
|
||||||
|
|
||||||
@@ -169,11 +169,13 @@ def test_data():
|
|||||||
metadata_1: CheckpointMetadata = {
|
metadata_1: CheckpointMetadata = {
|
||||||
"source": "input",
|
"source": "input",
|
||||||
"step": 2,
|
"step": 2,
|
||||||
|
"writes": {},
|
||||||
"score": 1,
|
"score": 1,
|
||||||
}
|
}
|
||||||
metadata_2: CheckpointMetadata = {
|
metadata_2: CheckpointMetadata = {
|
||||||
"source": "loop",
|
"source": "loop",
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
"score": None,
|
"score": None,
|
||||||
}
|
}
|
||||||
metadata_3: CheckpointMetadata = {}
|
metadata_3: CheckpointMetadata = {}
|
||||||
@@ -200,6 +202,7 @@ def test_combined_metadata(saver_name: str, test_data) -> None:
|
|||||||
metadata: CheckpointMetadata = {
|
metadata: CheckpointMetadata = {
|
||||||
"source": "loop",
|
"source": "loop",
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
"score": None,
|
"score": None,
|
||||||
}
|
}
|
||||||
saver.put(config, chkpnt, metadata, {})
|
saver.put(config, chkpnt, metadata, {})
|
||||||
@@ -225,6 +228,7 @@ def test_search(saver_name: str, test_data) -> None:
|
|||||||
query_1 = {"source": "input"} # search by 1 key
|
query_1 = {"source": "input"} # search by 1 key
|
||||||
query_2 = {
|
query_2 = {
|
||||||
"step": 1,
|
"step": 1,
|
||||||
|
"writes": {"foo": "bar"},
|
||||||
} # search by multiple keys
|
} # search by multiple keys
|
||||||
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
|
||||||
query_4 = {"source": "update", "step": 1} # no match
|
query_4 = {"source": "update", "step": 1} # no match
|
||||||
@@ -328,33 +332,3 @@ def test_pending_sends_migration(saver_name: str) -> None:
|
|||||||
TASKS: ["send-1", "send-2", "send-3"]
|
TASKS: ["send-1", "send-2", "send-3"]
|
||||||
}
|
}
|
||||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
|
||||||
def test_get_checkpoint_no_channel_values(
|
|
||||||
monkeypatch, saver_name: str, test_data
|
|
||||||
) -> None:
|
|
||||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
|
||||||
with _saver(saver_name) as saver:
|
|
||||||
config = {
|
|
||||||
"configurable": {
|
|
||||||
"thread_id": "thread-2",
|
|
||||||
"checkpoint_ns": "",
|
|
||||||
"__super_private_key": "super_private_value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
|
||||||
saver.put(config, chkpnt, {}, {})
|
|
||||||
|
|
||||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
|
||||||
|
|
||||||
def patched_load_checkpoint_tuple(value):
|
|
||||||
value["checkpoint"].pop("channel_values", None)
|
|
||||||
return load_checkpoint_tuple(value)
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
|
||||||
)
|
|
||||||
|
|
||||||
checkpoint = saver.get_tuple(config)
|
|
||||||
assert checkpoint.checkpoint["channel_values"] == {}
|
|
||||||
|
|||||||
Generated
+652
-624
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 LangChain, Inc.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
@@ -9,6 +8,7 @@ from contextlib import closing, contextmanager
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
BaseCheckpointSaver,
|
BaseCheckpointSaver,
|
||||||
@@ -21,7 +21,6 @@ from langgraph.checkpoint.base import (
|
|||||||
get_checkpoint_metadata,
|
get_checkpoint_metadata,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||||
|
|
||||||
from langgraph.checkpoint.sqlite.utils import search_where
|
from langgraph.checkpoint.sqlite.utils import search_where
|
||||||
|
|
||||||
_AIO_ERROR_MSG = (
|
_AIO_ERROR_MSG = (
|
||||||
@@ -185,7 +184,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -193,7 +192,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
@@ -266,7 +265,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self.serde.loads_typed((type, checkpoint)),
|
self.serde.loads_typed((type, checkpoint)),
|
||||||
cast(
|
cast(
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
json.loads(metadata) if metadata is not None else {},
|
self.jsonplus_serde.loads(metadata)
|
||||||
|
if metadata is not None
|
||||||
|
else {},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -300,12 +301,12 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: The config to use for listing the checkpoints.
|
config: The config to use for listing the checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata. Defaults to None.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: The maximum number of checkpoints to return.
|
limit: The maximum number of checkpoints to return. Defaults to None.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An iterator of checkpoint tuples.
|
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> from langgraph.checkpoint.sqlite import SqliteSaver
|
>>> from langgraph.checkpoint.sqlite import SqliteSaver
|
||||||
@@ -357,7 +358,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self.serde.loads_typed((type, checkpoint)),
|
self.serde.loads_typed((type, checkpoint)),
|
||||||
cast(
|
cast(
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
json.loads(metadata) if metadata is not None else {},
|
self.jsonplus_serde.loads(metadata)
|
||||||
|
if metadata is not None
|
||||||
|
else {},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -410,9 +413,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
thread_id = config["configurable"]["thread_id"]
|
thread_id = config["configurable"]["thread_id"]
|
||||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||||
serialized_metadata = json.dumps(
|
serialized_metadata = self.jsonplus_serde.dumps(
|
||||||
get_checkpoint_metadata(config, metadata), ensure_ascii=False
|
get_checkpoint_metadata(config, metadata)
|
||||||
).encode("utf-8", "ignore")
|
)
|
||||||
with self.cursor() as cur:
|
with self.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, Callable, TypeVar, cast
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
BaseCheckpointSaver,
|
BaseCheckpointSaver,
|
||||||
@@ -21,7 +21,6 @@ from langgraph.checkpoint.base import (
|
|||||||
get_checkpoint_metadata,
|
get_checkpoint_metadata,
|
||||||
)
|
)
|
||||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||||
|
|
||||||
from langgraph.checkpoint.sqlite.utils import search_where
|
from langgraph.checkpoint.sqlite.utils import search_where
|
||||||
|
|
||||||
T = TypeVar("T", bound=Callable)
|
T = TypeVar("T", bound=Callable)
|
||||||
@@ -140,7 +139,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -148,7 +147,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# check if we are in the main thread, only bg threads can block
|
# check if we are in the main thread, only bg threads can block
|
||||||
@@ -182,11 +181,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
Args:
|
Args:
|
||||||
config: Base configuration for filtering checkpoints.
|
config: Base configuration for filtering checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An iterator of matching checkpoint tuples.
|
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# check if we are in the main thread, only bg threads can block
|
# check if we are in the main thread, only bg threads can block
|
||||||
@@ -317,7 +316,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -325,7 +324,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
await self.setup()
|
await self.setup()
|
||||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||||
@@ -378,7 +377,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self.serde.loads_typed((type, checkpoint)),
|
self.serde.loads_typed((type, checkpoint)),
|
||||||
cast(
|
cast(
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
(json.loads(metadata) if metadata is not None else {}),
|
self.jsonplus_serde.loads(metadata)
|
||||||
|
if metadata is not None
|
||||||
|
else {},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -413,11 +414,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
Args:
|
Args:
|
||||||
config: Base configuration for filtering checkpoints.
|
config: Base configuration for filtering checkpoints.
|
||||||
filter: Additional filtering criteria for metadata.
|
filter: Additional filtering criteria for metadata.
|
||||||
before: If provided, only checkpoints before the specified checkpoint ID are returned.
|
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An asynchronous iterator of matching checkpoint tuples.
|
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
await self.setup()
|
await self.setup()
|
||||||
where, params = search_where(config, filter, before)
|
where, params = search_where(config, filter, before)
|
||||||
@@ -456,7 +457,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self.serde.loads_typed((type, checkpoint)),
|
self.serde.loads_typed((type, checkpoint)),
|
||||||
cast(
|
cast(
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
(json.loads(metadata) if metadata is not None else {}),
|
self.jsonplus_serde.loads(metadata)
|
||||||
|
if metadata is not None
|
||||||
|
else {},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -500,9 +503,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
thread_id = config["configurable"]["thread_id"]
|
thread_id = config["configurable"]["thread_id"]
|
||||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||||
serialized_metadata = json.dumps(
|
serialized_metadata = self.jsonplus_serde.dumps(
|
||||||
get_checkpoint_metadata(config, metadata), ensure_ascii=False
|
get_checkpoint_metadata(config, metadata)
|
||||||
).encode("utf-8", "ignore")
|
)
|
||||||
async with (
|
async with (
|
||||||
self.lock,
|
self.lock,
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from collections.abc import Sequence
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
from langgraph.checkpoint.base import get_checkpoint_id
|
from langgraph.checkpoint.base import get_checkpoint_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
import orjson
|
import orjson
|
||||||
import sqlite_vec # type: ignore[import-untyped]
|
import sqlite_vec # type: ignore[import-untyped]
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
ListNamespacesOp,
|
ListNamespacesOp,
|
||||||
@@ -21,7 +22,6 @@ from langgraph.store.base import (
|
|||||||
TTLConfig,
|
TTLConfig,
|
||||||
)
|
)
|
||||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||||
|
|
||||||
from langgraph.store.sqlite.base import (
|
from langgraph.store.sqlite.base import (
|
||||||
_PLACEHOLDER,
|
_PLACEHOLDER,
|
||||||
BaseSqliteStore,
|
BaseSqliteStore,
|
||||||
@@ -303,7 +303,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Maximum time to wait for the task to stop, in seconds.
|
timeout: Maximum time to wait for the task to stop, in seconds.
|
||||||
If `None`, wait indefinitely.
|
If None, wait indefinitely.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the task was successfully stopped or wasn't running,
|
bool: True if the task was successfully stopped or wasn't running,
|
||||||
@@ -484,7 +484,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
|
|
||||||
# Convert vectors to SQLite-friendly format
|
# Convert vectors to SQLite-friendly format
|
||||||
vector_params = []
|
vector_params = []
|
||||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
||||||
vector_params.extend(
|
vector_params.extend(
|
||||||
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
||||||
)
|
)
|
||||||
@@ -507,9 +507,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
results: List to store results in.
|
results: List to store results in.
|
||||||
cur: Database cursor.
|
cur: Database cursor.
|
||||||
"""
|
"""
|
||||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||||
search_ops
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup dot_product function if it doesn't exist
|
# Setup dot_product function if it doesn't exist
|
||||||
if embedding_requests and self.embeddings:
|
if embedding_requests and self.embeddings:
|
||||||
@@ -517,62 +515,23 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
[query for _, query in embedding_requests]
|
[query for _, query in embedding_requests]
|
||||||
)
|
)
|
||||||
|
|
||||||
for (embed_req_idx, _), embedding in zip(
|
for (idx, _), embedding in zip(embedding_requests, vectors):
|
||||||
embedding_requests, vectors, strict=False
|
_params_list: list = queries[idx][1]
|
||||||
):
|
|
||||||
# Find the corresponding query in prepared_queries
|
|
||||||
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries
|
|
||||||
if embed_req_idx < len(prepared_queries):
|
|
||||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
|
||||||
for i, param in enumerate(_params_list):
|
for i, param in enumerate(_params_list):
|
||||||
if param is _PLACEHOLDER:
|
if param is _PLACEHOLDER:
|
||||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
|
||||||
)
|
|
||||||
|
|
||||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||||
search_ops, prepared_queries, strict=False
|
|
||||||
):
|
|
||||||
await cur.execute(query, params)
|
await cur.execute(query, params)
|
||||||
rows = await cur.fetchall()
|
rows = await cur.fetchall()
|
||||||
|
|
||||||
if needs_refresh and rows and self.ttl_config:
|
if "score" in query:
|
||||||
keys_to_refresh = []
|
|
||||||
for row_data in rows:
|
|
||||||
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
|
|
||||||
# These are raw text values directly from the DB.
|
|
||||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
|
||||||
|
|
||||||
if keys_to_refresh:
|
|
||||||
updates_by_prefix = defaultdict(list)
|
|
||||||
for prefix_text, key_text in keys_to_refresh:
|
|
||||||
updates_by_prefix[prefix_text].append(key_text)
|
|
||||||
|
|
||||||
for prefix_text, key_list in updates_by_prefix.items():
|
|
||||||
placeholders = ",".join(["?"] * len(key_list))
|
|
||||||
update_query = f"""
|
|
||||||
UPDATE store
|
|
||||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
||||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
|
||||||
"""
|
|
||||||
update_params = (prefix_text, *key_list)
|
|
||||||
try:
|
|
||||||
await cur.execute(update_query, update_params)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error during TTL refresh update for search: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process rows into items
|
|
||||||
if "score" in query: # Vector search query
|
|
||||||
items = [
|
items = [
|
||||||
_row_to_search_item(
|
_row_to_search_item(
|
||||||
_decode_ns_text(row[0]), # prefix
|
_decode_ns_text(row[0]),
|
||||||
{
|
{
|
||||||
"key": row[1], # key
|
"key": row[1],
|
||||||
"value": row[2], # value
|
"value": row[2],
|
||||||
"created_at": row[3],
|
"created_at": row[3],
|
||||||
"updated_at": row[4],
|
"updated_at": row[4],
|
||||||
"expires_at": row[5] if len(row) > 5 else None,
|
"expires_at": row[5] if len(row) > 5 else None,
|
||||||
@@ -586,10 +545,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
else: # Regular search query
|
else: # Regular search query
|
||||||
items = [
|
items = [
|
||||||
_row_to_search_item(
|
_row_to_search_item(
|
||||||
_decode_ns_text(row[0]), # prefix
|
_decode_ns_text(row[0]),
|
||||||
{
|
{
|
||||||
"key": row[1], # key
|
"key": row[1],
|
||||||
"value": row[2], # value
|
"value": row[2],
|
||||||
"created_at": row[3],
|
"created_at": row[3],
|
||||||
"updated_at": row[4],
|
"updated_at": row[4],
|
||||||
"expires_at": row[5] if len(row) > 5 else None,
|
"expires_at": row[5] if len(row) > 5 else None,
|
||||||
@@ -600,7 +559,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
results[original_op_idx] = items
|
results[idx] = items
|
||||||
|
|
||||||
async def _batch_list_namespaces_ops(
|
async def _batch_list_namespaces_ops(
|
||||||
self,
|
self,
|
||||||
@@ -616,7 +575,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
cur: Database cursor.
|
cur: Database cursor.
|
||||||
"""
|
"""
|
||||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||||
await cur.execute(query, params)
|
await cur.execute(query, params)
|
||||||
|
|
||||||
rows = await cur.fetchall()
|
rows = await cur.fetchall()
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import re
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Literal, NamedTuple, cast
|
from typing import Any, Callable, Literal, NamedTuple, cast
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
import sqlite_vec # type: ignore[import-untyped]
|
import sqlite_vec # type: ignore[import-untyped]
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
BaseStore,
|
BaseStore,
|
||||||
GetOp,
|
GetOp,
|
||||||
@@ -232,7 +233,7 @@ class BaseSqliteStore:
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for namespace, items in namespace_groups.items():
|
for namespace, items in namespace_groups.items():
|
||||||
_, keys = zip(*items, strict=False)
|
_, keys = zip(*items)
|
||||||
this_refresh_ttls = refresh_ttls[namespace]
|
this_refresh_ttls = refresh_ttls[namespace]
|
||||||
refresh_ttl_any = any(this_refresh_ttls)
|
refresh_ttl_any = any(this_refresh_ttls)
|
||||||
|
|
||||||
@@ -371,15 +372,13 @@ class BaseSqliteStore:
|
|||||||
def _prepare_batch_search_queries(
|
def _prepare_batch_search_queries(
|
||||||
self, search_ops: Sequence[tuple[int, SearchOp]]
|
self, search_ops: Sequence[tuple[int, SearchOp]]
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
list[
|
list[tuple[str, list[None | str | list[float]]]], # queries, params
|
||||||
tuple[str, list[None | str | list[float]], bool]
|
|
||||||
], # queries, params, needs_refresh
|
|
||||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
|
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
||||||
Returns:
|
Returns:
|
||||||
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
|
- queries: list of (SQL, param_list)
|
||||||
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
||||||
"""
|
"""
|
||||||
queries = []
|
queries = []
|
||||||
@@ -520,18 +519,30 @@ class BaseSqliteStore:
|
|||||||
logger.debug(f"Search query: {base_query}")
|
logger.debug(f"Search query: {base_query}")
|
||||||
logger.debug(f"Search params: {params}")
|
logger.debug(f"Search params: {params}")
|
||||||
|
|
||||||
# Determine if TTL refresh is needed
|
# Handle TTL refresh if requested
|
||||||
needs_ttl_refresh = bool(
|
if (
|
||||||
op.refresh_ttl
|
op.refresh_ttl
|
||||||
and self.ttl_config
|
and self.ttl_config
|
||||||
and self.ttl_config.get("refresh_on_read", False)
|
and self.ttl_config.get("refresh_on_read", False)
|
||||||
|
):
|
||||||
|
final_sql = f"""
|
||||||
|
WITH search_results AS (
|
||||||
|
{base_query}
|
||||||
|
),
|
||||||
|
updated AS (
|
||||||
|
UPDATE store
|
||||||
|
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||||
|
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
|
||||||
|
AND ttl_minutes IS NOT NULL
|
||||||
)
|
)
|
||||||
|
SELECT * FROM search_results
|
||||||
# The base_query is now the final_sql, and we pass the refresh flag
|
"""
|
||||||
|
final_params = params[:] # copy params
|
||||||
|
else:
|
||||||
final_sql = base_query
|
final_sql = base_query
|
||||||
final_params = params
|
final_params = params
|
||||||
|
|
||||||
queries.append((final_sql, final_params, needs_ttl_refresh))
|
queries.append((final_sql, final_params))
|
||||||
|
|
||||||
return queries, embedding_requests
|
return queries, embedding_requests
|
||||||
|
|
||||||
@@ -829,7 +840,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
|
|
||||||
results = []
|
results = []
|
||||||
for namespace, items in namespace_groups.items():
|
for namespace, items in namespace_groups.items():
|
||||||
_, keys = zip(*items, strict=False)
|
_, keys = zip(*items)
|
||||||
this_refresh_ttls = refresh_ttls[namespace]
|
this_refresh_ttls = refresh_ttls[namespace]
|
||||||
refresh_ttl_any = any(this_refresh_ttls)
|
refresh_ttl_any = any(this_refresh_ttls)
|
||||||
|
|
||||||
@@ -1156,7 +1167,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Maximum time to wait for the thread to stop, in seconds.
|
timeout: Maximum time to wait for the thread to stop, in seconds.
|
||||||
If `None`, wait indefinitely.
|
If None, wait indefinitely.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the thread was successfully stopped or wasn't running,
|
bool: True if the thread was successfully stopped or wasn't running,
|
||||||
@@ -1304,7 +1315,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
|
|
||||||
# Convert vectors to SQLite-friendly format
|
# Convert vectors to SQLite-friendly format
|
||||||
vector_params = []
|
vector_params = []
|
||||||
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
|
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
|
||||||
vector_params.extend(
|
vector_params.extend(
|
||||||
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
|
||||||
)
|
)
|
||||||
@@ -1320,9 +1331,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
results: list[Result],
|
results: list[Result],
|
||||||
cur: sqlite3.Cursor,
|
cur: sqlite3.Cursor,
|
||||||
) -> None:
|
) -> None:
|
||||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||||
search_ops
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup similarity functions if they don't exist
|
# Setup similarity functions if they don't exist
|
||||||
if embedding_requests and self.embeddings:
|
if embedding_requests and self.embeddings:
|
||||||
@@ -1332,50 +1341,16 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Replace placeholders with actual embeddings
|
# Replace placeholders with actual embeddings
|
||||||
for (embed_req_idx, _), embedding in zip(
|
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||||
embedding_requests, embeddings, strict=False
|
_params_list: list = queries[idx][1]
|
||||||
):
|
|
||||||
if embed_req_idx < len(prepared_queries):
|
|
||||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
|
||||||
for i, param in enumerate(_params_list):
|
for i, param in enumerate(_params_list):
|
||||||
if param is _PLACEHOLDER:
|
if param is _PLACEHOLDER:
|
||||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
|
||||||
)
|
|
||||||
|
|
||||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||||
search_ops, prepared_queries, strict=False
|
|
||||||
):
|
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
if needs_refresh and rows and self.ttl_config:
|
|
||||||
keys_to_refresh = []
|
|
||||||
for row_data in rows:
|
|
||||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
|
||||||
|
|
||||||
if keys_to_refresh:
|
|
||||||
updates_by_prefix = defaultdict(list)
|
|
||||||
for prefix_text, key_text in keys_to_refresh:
|
|
||||||
updates_by_prefix[prefix_text].append(key_text)
|
|
||||||
|
|
||||||
for prefix_text, key_list in updates_by_prefix.items():
|
|
||||||
placeholders = ",".join(["?"] * len(key_list))
|
|
||||||
update_query = f"""
|
|
||||||
UPDATE store
|
|
||||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
||||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
|
||||||
"""
|
|
||||||
update_params = (prefix_text, *key_list)
|
|
||||||
try:
|
|
||||||
cur.execute(update_query, update_params)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error during TTL refresh update for search: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "score" in query: # Vector search query
|
if "score" in query: # Vector search query
|
||||||
items = [
|
items = [
|
||||||
_row_to_search_item(
|
_row_to_search_item(
|
||||||
@@ -1410,7 +1385,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
results[original_op_idx] = items
|
results[idx] = items
|
||||||
|
|
||||||
def _batch_list_namespaces_ops(
|
def _batch_list_namespaces_ops(
|
||||||
self,
|
self,
|
||||||
@@ -1419,7 +1394,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
cur: sqlite3.Cursor,
|
cur: sqlite3.Cursor,
|
||||||
) -> None:
|
) -> None:
|
||||||
queries = self._get_batch_list_namespaces_queries(list_ops)
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
||||||
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
|
for (query, params), (idx, _) in zip(queries, list_ops):
|
||||||
cur.execute(query, params)
|
cur.execute(query, params)
|
||||||
results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
|
results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
|
||||||
|
|
||||||
|
|||||||
@@ -4,43 +4,34 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "langgraph-checkpoint-sqlite"
|
name = "langgraph-checkpoint-sqlite"
|
||||||
version = "3.0.0"
|
version = "2.0.11"
|
||||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||||
authors = []
|
authors = []
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.9"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ['LICENSE']
|
license-files = ['LICENSE']
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"langgraph-checkpoint>=3,<4.0.0",
|
"langgraph-checkpoint>=2.0.21,<3.0.0",
|
||||||
"aiosqlite>=0.20",
|
"aiosqlite>=0.20",
|
||||||
"sqlite-vec>=0.1.6",
|
"sqlite-vec>=0.1.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
|
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||||
Twitter = "https://x.com/LangChainAI"
|
|
||||||
Slack = "https://www.langchain.com/join-community"
|
|
||||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
test = [
|
dev = [
|
||||||
|
"ruff",
|
||||||
|
"codespell",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
"pytest-watcher",
|
"pytest-watcher",
|
||||||
|
"mypy",
|
||||||
"langgraph-checkpoint",
|
"langgraph-checkpoint",
|
||||||
"pytest-retry>=1.7.0",
|
"pytest-retry>=1.7.0",
|
||||||
]
|
]
|
||||||
lint = [
|
|
||||||
"ruff",
|
|
||||||
"codespell",
|
|
||||||
"mypy",
|
|
||||||
]
|
|
||||||
dev = [
|
|
||||||
{include-group = "test"},
|
|
||||||
{include-group = "lint"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
default-groups = ['dev']
|
default-groups = ['dev']
|
||||||
@@ -62,10 +53,8 @@ lint.select = [
|
|||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
"UP", # pyupgrade
|
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008"]
|
lint.ignore = ["E501", "B008"]
|
||||||
target-version = "py310"
|
|
||||||
|
|
||||||
[tool.pytest-watcher]
|
[tool.pytest-watcher]
|
||||||
now = true
|
now = true
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
create_checkpoint,
|
create_checkpoint,
|
||||||
empty_checkpoint,
|
empty_checkpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncIterator, Generator, Iterable
|
from collections.abc import AsyncIterator, Generator, Iterable
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import cast
|
from typing import Optional, Union, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
Item,
|
Item,
|
||||||
@@ -15,7 +16,6 @@ from langgraph.store.base import (
|
|||||||
PutOp,
|
PutOp,
|
||||||
SearchOp,
|
SearchOp,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langgraph.store.sqlite import AsyncSqliteStore
|
from langgraph.store.sqlite import AsyncSqliteStore
|
||||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||||
from tests.test_store import CharacterEmbeddings
|
from tests.test_store import CharacterEmbeddings
|
||||||
@@ -51,7 +51,7 @@ def fake_embeddings() -> CharacterEmbeddings:
|
|||||||
async def create_vector_store(
|
async def create_vector_store(
|
||||||
fake_embeddings: CharacterEmbeddings,
|
fake_embeddings: CharacterEmbeddings,
|
||||||
conn_string: str = ":memory:",
|
conn_string: str = ":memory:",
|
||||||
text_fields: list[str] | None = None,
|
text_fields: Optional[list[str]] = None,
|
||||||
) -> AsyncIterator[AsyncSqliteStore]:
|
) -> AsyncIterator[AsyncSqliteStore]:
|
||||||
"""Create an AsyncSqliteStore with vector search capabilities."""
|
"""Create an AsyncSqliteStore with vector search capabilities."""
|
||||||
index_config: SqliteIndexConfig = {
|
index_config: SqliteIndexConfig = {
|
||||||
@@ -168,7 +168,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
results = await store.abatch(
|
results = await store.abatch(
|
||||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||||
)
|
)
|
||||||
assert len(results) == 5
|
assert len(results) == 5
|
||||||
assert isinstance(results[0], Item)
|
assert isinstance(results[0], Item)
|
||||||
@@ -193,7 +193,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
results_reordered = await store.abatch(
|
results_reordered = await store.abatch(
|
||||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
|
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
|
||||||
)
|
)
|
||||||
assert len(results_reordered) == 5
|
assert len(results_reordered) == 5
|
||||||
assert isinstance(results_reordered[0], list)
|
assert isinstance(results_reordered[0], list)
|
||||||
@@ -681,7 +681,7 @@ async def test_search_items(
|
|||||||
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
||||||
) as store:
|
) as store:
|
||||||
# Insert test data
|
# Insert test data
|
||||||
for ns, item in zip(test_namespaces, test_items, strict=False):
|
for ns, item in zip(test_namespaces, test_items):
|
||||||
key = f"item_{ns[-1]}"
|
key = f"item_{ns[-1]}"
|
||||||
await store.aput(ns, key, item)
|
await store.aput(ns, key, item)
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from typing import Any, cast
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
CheckpointMetadata,
|
CheckpointMetadata,
|
||||||
create_checkpoint,
|
create_checkpoint,
|
||||||
empty_checkpoint,
|
empty_checkpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||||
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
|
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
|
||||||
|
|
||||||
@@ -116,17 +116,7 @@ class TestSqliteSaver:
|
|||||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||||
} == {"", "inner"}
|
} == {"", "inner"}
|
||||||
|
|
||||||
# search with before param
|
# TODO: test before and limit params
|
||||||
search_results_6 = list(saver.list(None, before=search_results_5[1].config))
|
|
||||||
assert len(search_results_6) == 1
|
|
||||||
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
|
|
||||||
|
|
||||||
# search with limit param
|
|
||||||
search_results_7 = list(
|
|
||||||
saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
|
|
||||||
)
|
|
||||||
assert len(search_results_7) == 1
|
|
||||||
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
|
|
||||||
|
|
||||||
def test_search_where(self) -> None:
|
def test_search_where(self) -> None:
|
||||||
# call method / assertions
|
# call method / assertions
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Generator, Iterable
|
from collections.abc import Generator, Iterable
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Literal, cast
|
from typing import Any, Literal, Optional, Union, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
GetOp,
|
GetOp,
|
||||||
Item,
|
Item,
|
||||||
@@ -17,7 +18,6 @@ from langgraph.store.base import (
|
|||||||
PutOp,
|
PutOp,
|
||||||
SearchOp,
|
SearchOp,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langgraph.store.sqlite import SqliteStore
|
from langgraph.store.sqlite import SqliteStore
|
||||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
|
|||||||
@contextmanager
|
@contextmanager
|
||||||
def create_vector_store(
|
def create_vector_store(
|
||||||
fake_embeddings: CharacterEmbeddings,
|
fake_embeddings: CharacterEmbeddings,
|
||||||
text_fields: list[str] | None = None,
|
text_fields: Optional[list[str]] = None,
|
||||||
distance_type: str = "cosine",
|
distance_type: str = "cosine",
|
||||||
conn_type: Literal["memory", "file"] = "memory",
|
conn_type: Literal["memory", "file"] = "memory",
|
||||||
) -> Generator[SqliteStore, None, None]:
|
) -> Generator[SqliteStore, None, None]:
|
||||||
@@ -153,7 +153,7 @@ def test_batch_order(store: SqliteStore) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
results = store.batch(
|
results = store.batch(
|
||||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||||
)
|
)
|
||||||
assert len(results) == 5
|
assert len(results) == 5
|
||||||
assert isinstance(results[0], Item)
|
assert isinstance(results[0], Item)
|
||||||
@@ -182,7 +182,7 @@ def test_batch_order(store: SqliteStore) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
results_reordered = store.batch(
|
results_reordered = store.batch(
|
||||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
|
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
|
||||||
)
|
)
|
||||||
assert len(results_reordered) == 5
|
assert len(results_reordered) == 5
|
||||||
assert isinstance(results_reordered[0], list)
|
assert isinstance(results_reordered[0], list)
|
||||||
@@ -301,7 +301,7 @@ def test_batch_list_namespaces_ops(store: SqliteStore) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
results = store.batch(
|
results = store.batch(
|
||||||
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
|
||||||
)
|
)
|
||||||
assert len(results) == 3
|
assert len(results) == 3
|
||||||
|
|
||||||
@@ -778,7 +778,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
|||||||
|
|
||||||
similarities = []
|
similarities = []
|
||||||
for y in Y:
|
for y in Y:
|
||||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
dot_product = sum(a * b for a, b in zip(X, y))
|
||||||
norm1 = sum(a * a for a in X) ** 0.5
|
norm1 = sum(a * a for a in X) ** 0.5
|
||||||
norm2 = sum(a * a for a in y) ** 0.5
|
norm2 = sum(a * a for a in y) ** 0.5
|
||||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||||
@@ -1011,7 +1011,7 @@ def test_search_items(
|
|||||||
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
||||||
) as store:
|
) as store:
|
||||||
# Insert test data
|
# Insert test data
|
||||||
for ns, item in zip(test_namespaces, test_items, strict=False):
|
for ns, item in zip(test_namespaces, test_items):
|
||||||
key = f"item_{ns[-1]}"
|
key = f"item_{ns[-1]}"
|
||||||
store.put(ns, key, item)
|
store.put(ns, key, item)
|
||||||
|
|
||||||
@@ -1067,31 +1067,3 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
|
|||||||
|
|
||||||
with pytest.raises(ValueError, match="Invalid filter key"):
|
with pytest.raises(ValueError, match="Invalid filter key"):
|
||||||
store.search(("docs",), filter={malicious_key: "dummy"})
|
store.search(("docs",), filter={malicious_key: "dummy"})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
||||||
def test_non_ascii(
|
|
||||||
fake_embeddings: CharacterEmbeddings,
|
|
||||||
distance_type: str,
|
|
||||||
) -> None:
|
|
||||||
"""Test support for non-ascii characters"""
|
|
||||||
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
|
||||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
|
||||||
store.put(
|
|
||||||
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
|
||||||
) # Japanese
|
|
||||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
|
||||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
|
||||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
|
||||||
|
|
||||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
|
||||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
|
||||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
|
||||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
|
||||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
|
||||||
|
|
||||||
assert result1[0].key == "1"
|
|
||||||
assert result2[0].key == "2"
|
|
||||||
assert result3[0].key == "3"
|
|
||||||
assert result4[0].key == "4"
|
|
||||||
assert result5[0].key == "5"
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import time
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langgraph.store.base import TTLConfig
|
|
||||||
|
|
||||||
from langgraph.store.sqlite import SqliteStore
|
from langgraph.store.sqlite import SqliteStore
|
||||||
from langgraph.store.sqlite.aio import AsyncSqliteStore
|
from langgraph.store.sqlite.aio import AsyncSqliteStore
|
||||||
@@ -94,13 +93,9 @@ def test_ttl_sweeper(temp_db_file: str) -> None:
|
|||||||
ttl_seconds = 2
|
ttl_seconds = 2
|
||||||
ttl_minutes = ttl_seconds / 60
|
ttl_minutes = ttl_seconds / 60
|
||||||
|
|
||||||
ttl_config: TTLConfig = {
|
|
||||||
"default_ttl": ttl_minutes,
|
|
||||||
"sweep_interval_minutes": ttl_minutes / 2,
|
|
||||||
}
|
|
||||||
with SqliteStore.from_conn_string(
|
with SqliteStore.from_conn_string(
|
||||||
temp_db_file,
|
temp_db_file,
|
||||||
ttl=ttl_config,
|
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||||
) as store:
|
) as store:
|
||||||
store.setup()
|
store.setup()
|
||||||
|
|
||||||
@@ -303,14 +298,9 @@ async def test_async_ttl_sweeper(temp_db_file: str) -> None:
|
|||||||
ttl_seconds = 2
|
ttl_seconds = 2
|
||||||
ttl_minutes = ttl_seconds / 60
|
ttl_minutes = ttl_seconds / 60
|
||||||
|
|
||||||
ttl_config: TTLConfig = {
|
|
||||||
"default_ttl": ttl_minutes,
|
|
||||||
"sweep_interval_minutes": ttl_minutes / 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
async with AsyncSqliteStore.from_conn_string(
|
async with AsyncSqliteStore.from_conn_string(
|
||||||
temp_db_file,
|
temp_db_file,
|
||||||
ttl=ttl_config,
|
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||||
) as store:
|
) as store:
|
||||||
await store.setup()
|
await store.setup()
|
||||||
|
|
||||||
@@ -363,67 +353,3 @@ async def test_async_search_with_ttl(temp_db_file: str) -> None:
|
|||||||
# Search after expiration
|
# Search after expiration
|
||||||
results = await store.asearch(("test",), filter={"value": "apple"})
|
results = await store.asearch(("test",), filter={"value": "apple"})
|
||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.flaky(retries=3)
|
|
||||||
async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
|
|
||||||
"""Test TTL refresh on asearch with async API."""
|
|
||||||
ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
|
|
||||||
ttl_minutes = ttl_seconds / 60.0
|
|
||||||
|
|
||||||
async with AsyncSqliteStore.from_conn_string(
|
|
||||||
temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
|
|
||||||
) as store:
|
|
||||||
await store.setup()
|
|
||||||
|
|
||||||
namespace = ("docs", "user1")
|
|
||||||
# t=0: items put, expire at t=4.0s
|
|
||||||
await store.aput(namespace, "item1", {"text": "content1", "id": 1})
|
|
||||||
await store.aput(namespace, "item2", {"text": "content2", "id": 2})
|
|
||||||
|
|
||||||
# t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
|
|
||||||
await asyncio.sleep(ttl_seconds * 0.75)
|
|
||||||
|
|
||||||
# Perform asearch with refresh_ttl=True for item1.
|
|
||||||
# item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
|
|
||||||
# item2's TTL is not affected. Expires at t=4.0s.
|
|
||||||
searched_items = await store.asearch(
|
|
||||||
namespace, filter={"id": 1}, refresh_ttl=True
|
|
||||||
)
|
|
||||||
assert len(searched_items) == 1
|
|
||||||
assert searched_items[0].key == "item1"
|
|
||||||
|
|
||||||
# t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
|
|
||||||
await asyncio.sleep(ttl_seconds * 0.5)
|
|
||||||
# At this point:
|
|
||||||
# - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
|
|
||||||
# - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
|
|
||||||
|
|
||||||
await store.sweep_ttl()
|
|
||||||
|
|
||||||
# Check item1 (should exist due to asearch refresh)
|
|
||||||
item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
|
|
||||||
assert item1_check1 is not None, (
|
|
||||||
"Item1 should exist after asearch refresh and first sweep"
|
|
||||||
)
|
|
||||||
assert item1_check1.value["text"] == "content1"
|
|
||||||
|
|
||||||
# Check item2 (should be gone)
|
|
||||||
item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
|
|
||||||
assert item2_check1 is None, (
|
|
||||||
"Item2 should be gone after its original TTL expired"
|
|
||||||
)
|
|
||||||
|
|
||||||
# t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
|
|
||||||
await asyncio.sleep(ttl_seconds * 0.625)
|
|
||||||
# At this point:
|
|
||||||
# - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
|
|
||||||
|
|
||||||
await store.sweep_ttl()
|
|
||||||
|
|
||||||
# Check item1 again (should be gone now)
|
|
||||||
item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
|
|
||||||
assert item1_final_check is None, (
|
|
||||||
"Item1 should be gone after its refreshed TTL expired"
|
|
||||||
)
|
|
||||||
|
|||||||
Generated
+593
-573
File diff suppressed because it is too large
Load Diff
@@ -38,10 +38,8 @@ Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSav
|
|||||||
- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
|
- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
|
||||||
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
|
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
|
||||||
- `.list` - List checkpoints that match a given configuration and filter criteria.
|
- `.list` - List checkpoints that match a given configuration and filter criteria.
|
||||||
- `.delete_thread()` - Delete all checkpoints and writes associated with a thread.
|
|
||||||
- `.get_next_version()` - Generate the next version ID for a channel.
|
|
||||||
|
|
||||||
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). Similarly, the checkpointer must implement `.adelete_thread()` if asynchronous thread cleanup is desired. The base class provides a default implementation of `.get_next_version()` that generates an integer sequence starting from 1, but this method should be overridden for custom versioning schemes.
|
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import ( # noqa: UP035
|
|||||||
NamedTuple,
|
NamedTuple,
|
||||||
TypedDict,
|
TypedDict,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
@@ -34,17 +35,17 @@ class CheckpointMetadata(TypedDict, total=False):
|
|||||||
source: Literal["input", "loop", "update", "fork"]
|
source: Literal["input", "loop", "update", "fork"]
|
||||||
"""The source of the checkpoint.
|
"""The source of the checkpoint.
|
||||||
|
|
||||||
- `"input"`: The checkpoint was created from an input to invoke/stream/batch.
|
- "input": The checkpoint was created from an input to invoke/stream/batch.
|
||||||
- `"loop"`: The checkpoint was created from inside the pregel loop.
|
- "loop": The checkpoint was created from inside the pregel loop.
|
||||||
- `"update"`: The checkpoint was created from a manual state update.
|
- "update": The checkpoint was created from a manual state update.
|
||||||
- `"fork"`: The checkpoint was created as a copy of another checkpoint.
|
- "fork": The checkpoint was created as a copy of another checkpoint.
|
||||||
"""
|
"""
|
||||||
step: int
|
step: int
|
||||||
"""The step number of the checkpoint.
|
"""The step number of the checkpoint.
|
||||||
|
|
||||||
`-1` for the first `"input"` checkpoint.
|
-1 for the first "input" checkpoint.
|
||||||
`0` for the first `"loop"` checkpoint.
|
0 for the first "loop" checkpoint.
|
||||||
`...` for the `nth` checkpoint afterwards.
|
... for the nth checkpoint afterwards.
|
||||||
"""
|
"""
|
||||||
parents: dict[str, str]
|
parents: dict[str, str]
|
||||||
"""The IDs of the parent checkpoints.
|
"""The IDs of the parent checkpoints.
|
||||||
@@ -53,7 +54,7 @@ class CheckpointMetadata(TypedDict, total=False):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
ChannelVersions = dict[str, str | int | float]
|
ChannelVersions = dict[str, Union[str, int, float]]
|
||||||
|
|
||||||
|
|
||||||
class Checkpoint(TypedDict):
|
class Checkpoint(TypedDict):
|
||||||
@@ -147,7 +148,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
config: Configuration specifying which checkpoint to retrieve.
|
config: Configuration specifying which checkpoint to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested checkpoint, or `None` if not found.
|
Optional[Checkpoint]: The requested checkpoint, or None if not found.
|
||||||
"""
|
"""
|
||||||
if value := self.get_tuple(config):
|
if value := self.get_tuple(config):
|
||||||
return value.checkpoint
|
return value.checkpoint
|
||||||
@@ -159,7 +160,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
config: Configuration specifying which checkpoint to retrieve.
|
config: Configuration specifying which checkpoint to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested checkpoint tuple, or `None` if not found.
|
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||||
@@ -183,7 +184,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Iterator of matching checkpoint tuples.
|
Iterator[CheckpointTuple]: Iterator of matching checkpoint tuples.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||||
@@ -251,7 +252,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
config: Configuration specifying which checkpoint to retrieve.
|
config: Configuration specifying which checkpoint to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested checkpoint, or `None` if not found.
|
Optional[Checkpoint]: The requested checkpoint, or None if not found.
|
||||||
"""
|
"""
|
||||||
if value := await self.aget_tuple(config):
|
if value := await self.aget_tuple(config):
|
||||||
return value.checkpoint
|
return value.checkpoint
|
||||||
@@ -263,7 +264,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
config: Configuration specifying which checkpoint to retrieve.
|
config: Configuration specifying which checkpoint to retrieve.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The requested checkpoint tuple, or `None` if not found.
|
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||||
@@ -287,7 +288,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Async iterator of matching checkpoint tuples.
|
AsyncIterator[CheckpointTuple]: Async iterator of matching checkpoint tuples.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: Implement this method in your custom checkpoint saver.
|
NotImplementedError: Implement this method in your custom checkpoint saver.
|
||||||
@@ -352,11 +353,11 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
def get_next_version(self, current: V | None, channel: None) -> V:
|
def get_next_version(self, current: V | None, channel: None) -> V:
|
||||||
"""Generate the next version ID for a channel.
|
"""Generate the next version ID for a channel.
|
||||||
|
|
||||||
Default is to use integer versions, incrementing by `1`. If you override, you can use `str`/`int`/`float`
|
Default is to use integer versions, incrementing by 1. If you override, you can use str/int/float versions,
|
||||||
versions, as long as they are monotonically increasing.
|
as long as they are monotonically increasing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
current: The current version identifier (`int`, `float`, or `str`).
|
current: The current version identifier (int, float, or str).
|
||||||
channel: Deprecated argument, kept for backwards compatibility.
|
channel: Deprecated argument, kept for backwards compatibility.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -403,16 +404,6 @@ def get_checkpoint_metadata(
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
def get_serializable_checkpoint_metadata(
|
|
||||||
config: RunnableConfig, metadata: CheckpointMetadata
|
|
||||||
) -> CheckpointMetadata:
|
|
||||||
"""Get checkpoint metadata in a backwards-compatible manner."""
|
|
||||||
checkpoint_metadata = get_checkpoint_metadata(config, metadata)
|
|
||||||
if "writes" in checkpoint_metadata:
|
|
||||||
checkpoint_metadata.pop("writes")
|
|
||||||
return checkpoint_metadata
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Mapping from error type to error index.
|
Mapping from error type to error index.
|
||||||
Regular writes just map to their index in the list of writes being saved.
|
Regular writes just map to their index in the list of writes being saved.
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class InMemorySaver(
|
|||||||
Only use `InMemorySaver` for debugging or testing purposes.
|
Only use `InMemorySaver` for debugging or testing purposes.
|
||||||
For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
|
For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
|
||||||
|
|
||||||
If you are using LangSmith Deployment, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
|
If you are using the LangGraph Platform, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
serde: The serializer to use for serializing and deserializing checkpoints.
|
serde: The serializer to use for serializing and deserializing checkpoints. Defaults to None.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class InMemorySaver(
|
|||||||
"""Get a checkpoint tuple from the in-memory storage.
|
"""Get a checkpoint tuple from the in-memory storage.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the in-memory storage based on the
|
This method retrieves a checkpoint tuple from the in-memory storage based on the
|
||||||
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
|
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||||
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
|
||||||
for the given thread ID is retrieved.
|
for the given thread ID is retrieved.
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ class InMemorySaver(
|
|||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
thread_id: str = config["configurable"]["thread_id"]
|
thread_id: str = config["configurable"]["thread_id"]
|
||||||
checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
|
checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
|
||||||
@@ -231,7 +231,7 @@ class InMemorySaver(
|
|||||||
limit: Maximum number of checkpoints to return.
|
limit: Maximum number of checkpoints to return.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An iterator of matching checkpoint tuples.
|
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
|
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
|
||||||
config_checkpoint_ns = (
|
config_checkpoint_ns = (
|
||||||
@@ -423,16 +423,16 @@ class InMemorySaver(
|
|||||||
del self.blobs[k]
|
del self.blobs[k]
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Asynchronous version of `get_tuple`.
|
"""Asynchronous version of get_tuple.
|
||||||
|
|
||||||
This method is an asynchronous wrapper around `get_tuple` that runs the synchronous
|
This method is an asynchronous wrapper around get_tuple that runs the synchronous
|
||||||
method in a separate thread using asyncio.
|
method in a separate thread using asyncio.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: The config to use for retrieving the checkpoint.
|
config: The config to use for retrieving the checkpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||||
"""
|
"""
|
||||||
return self.get_tuple(config)
|
return self.get_tuple(config)
|
||||||
|
|
||||||
@@ -444,16 +444,16 @@ class InMemorySaver(
|
|||||||
before: RunnableConfig | None = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""Asynchronous version of `list`.
|
"""Asynchronous version of list.
|
||||||
|
|
||||||
This method is an asynchronous wrapper around `list` that runs the synchronous
|
This method is an asynchronous wrapper around list that runs the synchronous
|
||||||
method in a separate thread using asyncio.
|
method in a separate thread using asyncio.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: The config to use for listing the checkpoints.
|
config: The config to use for listing the checkpoints.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
An asynchronous iterator of checkpoint tuples.
|
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
|
||||||
"""
|
"""
|
||||||
for item in self.list(config, filter=filter, before=before, limit=limit):
|
for item in self.list(config, filter=filter, before=before, limit=limit):
|
||||||
yield item
|
yield item
|
||||||
@@ -465,7 +465,7 @@ class InMemorySaver(
|
|||||||
metadata: CheckpointMetadata,
|
metadata: CheckpointMetadata,
|
||||||
new_versions: ChannelVersions,
|
new_versions: ChannelVersions,
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
"""Asynchronous version of `put`.
|
"""Asynchronous version of put.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: The config to associate with the checkpoint.
|
config: The config to associate with the checkpoint.
|
||||||
@@ -485,9 +485,9 @@ class InMemorySaver(
|
|||||||
task_id: str,
|
task_id: str,
|
||||||
task_path: str = "",
|
task_path: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Asynchronous version of `put_writes`.
|
"""Asynchronous version of put_writes.
|
||||||
|
|
||||||
This method is an asynchronous wrapper around `put_writes` that runs the synchronous
|
This method is an asynchronous wrapper around put_writes that runs the synchronous
|
||||||
method in a separate thread using asyncio.
|
method in a separate thread using asyncio.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Protocol, runtime_checkable
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
|
||||||
class UntypedSerializerProtocol(Protocol):
|
class UntypedSerializerProtocol(Protocol):
|
||||||
@@ -11,14 +11,13 @@ class UntypedSerializerProtocol(Protocol):
|
|||||||
def loads(self, data: bytes) -> Any: ...
|
def loads(self, data: bytes) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
class SerializerProtocol(UntypedSerializerProtocol, Protocol):
|
||||||
class SerializerProtocol(Protocol):
|
|
||||||
"""Protocol for serialization and deserialization of objects.
|
"""Protocol for serialization and deserialization of objects.
|
||||||
|
|
||||||
- `dumps`: Serialize an object to bytes.
|
- `dumps`: Serialize an object to bytes.
|
||||||
- `dumps_typed`: Serialize an object to a tuple `(type, bytes)`.
|
- `dumps_typed`: Serialize an object to a tuple (type, bytes).
|
||||||
- `loads`: Deserialize an object from bytes.
|
- `loads`: Deserialize an object from bytes.
|
||||||
- `loads_typed`: Deserialize an object from a tuple `(type, bytes)`.
|
- `loads_typed`: Deserialize an object from a tuple (type, bytes).
|
||||||
|
|
||||||
Valid implementations include the `pickle`, `json` and `orjson` modules.
|
Valid implementations include the `pickle`, `json` and `orjson` modules.
|
||||||
"""
|
"""
|
||||||
@@ -32,6 +31,12 @@ class SerializerCompat(SerializerProtocol):
|
|||||||
def __init__(self, serde: UntypedSerializerProtocol) -> None:
|
def __init__(self, serde: UntypedSerializerProtocol) -> None:
|
||||||
self.serde = serde
|
self.serde = serde
|
||||||
|
|
||||||
|
def dumps(self, obj: Any) -> bytes:
|
||||||
|
return self.serde.dumps(obj)
|
||||||
|
|
||||||
|
def loads(self, data: bytes) -> Any:
|
||||||
|
return self.serde.loads(data)
|
||||||
|
|
||||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||||
return type(obj).__name__, self.serde.dumps(obj)
|
return type(obj).__name__, self.serde.dumps(obj)
|
||||||
|
|
||||||
@@ -44,7 +49,7 @@ def maybe_add_typed_methods(
|
|||||||
) -> SerializerProtocol:
|
) -> SerializerProtocol:
|
||||||
"""Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
|
"""Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
|
||||||
|
|
||||||
if not isinstance(serde, SerializerProtocol):
|
if not hasattr(serde, "loads_typed") or not hasattr(serde, "dumps_typed"):
|
||||||
return SerializerCompat(serde)
|
return SerializerCompat(serde)
|
||||||
|
|
||||||
return serde
|
return serde
|
||||||
|
|||||||
@@ -14,8 +14,14 @@ class EncryptedSerializer(SerializerProtocol):
|
|||||||
self.cipher = cipher
|
self.cipher = cipher
|
||||||
self.serde = serde
|
self.serde = serde
|
||||||
|
|
||||||
|
def dumps(self, obj: Any) -> bytes:
|
||||||
|
return self.serde.dumps(obj)
|
||||||
|
|
||||||
|
def loads(self, data: bytes) -> Any:
|
||||||
|
return self.serde.loads(data)
|
||||||
|
|
||||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||||
"""Serialize an object to a tuple `(type, bytes)` and encrypt the bytes."""
|
"""Serialize an object to a tuple (type, bytes) and encrypt the bytes."""
|
||||||
# serialize data
|
# serialize data
|
||||||
typ, data = self.serde.dumps_typed(obj)
|
typ, data = self.serde.dumps_typed(obj)
|
||||||
# encrypt data
|
# encrypt data
|
||||||
@@ -39,7 +45,7 @@ class EncryptedSerializer(SerializerProtocol):
|
|||||||
def from_pycryptodome_aes(
|
def from_pycryptodome_aes(
|
||||||
cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
|
cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
|
||||||
) -> "EncryptedSerializer":
|
) -> "EncryptedSerializer":
|
||||||
"""Create an `EncryptedSerializer` using AES encryption."""
|
"""Create an EncryptedSerializer using AES encryption."""
|
||||||
try:
|
try:
|
||||||
from Crypto.Cipher import AES # type: ignore
|
from Crypto.Cipher import AES # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import dataclasses
|
|||||||
import decimal
|
import decimal
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import date, datetime, time, timedelta, timezone
|
from datetime import date, datetime, time, timedelta, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
@@ -22,12 +21,13 @@ from ipaddress import (
|
|||||||
IPv6Interface,
|
IPv6Interface,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
)
|
)
|
||||||
from typing import Any, Literal
|
from typing import Any, Callable, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import ormsgpack
|
import ormsgpack
|
||||||
from langchain_core.load.load import Reviver
|
from langchain_core.load.load import Reviver
|
||||||
|
from langchain_core.load.serializable import Serializable
|
||||||
|
|
||||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||||
from langgraph.checkpoint.serde.types import SendProtocol
|
from langgraph.checkpoint.serde.types import SendProtocol
|
||||||
@@ -35,31 +35,18 @@ from langgraph.store.base import Item
|
|||||||
|
|
||||||
LC_REVIVER = Reviver()
|
LC_REVIVER = Reviver()
|
||||||
EMPTY_BYTES = b""
|
EMPTY_BYTES = b""
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonPlusSerializer(SerializerProtocol):
|
class JsonPlusSerializer(SerializerProtocol):
|
||||||
"""Serializer that uses ormsgpack, with optional fallbacks.
|
"""Serializer that uses ormsgpack, with a fallback to extended JSON serializer."""
|
||||||
|
|
||||||
Security note: this serializer is intended for use within the BaseCheckpointSaver
|
|
||||||
class and called within the Pregel loop. It should not be used on untrusted
|
|
||||||
python objects. If an attacker can write directly to your checkpoint database,
|
|
||||||
they may be able to trigger code execution when data is deserialized.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
pickle_fallback: bool = False,
|
pickle_fallback: bool = False,
|
||||||
allowed_json_modules: Sequence[tuple[str, ...]] | Literal[True] | None = None,
|
|
||||||
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
|
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.pickle_fallback = pickle_fallback
|
self.pickle_fallback = pickle_fallback
|
||||||
self._allowed_modules = (
|
|
||||||
{mod_and_name for mod_and_name in allowed_json_modules}
|
|
||||||
if allowed_json_modules and allowed_json_modules is not True
|
|
||||||
else (allowed_json_modules if allowed_json_modules is True else None)
|
|
||||||
)
|
|
||||||
self._unpack_ext_hook = (
|
self._unpack_ext_hook = (
|
||||||
__unpack_ext_hook__
|
__unpack_ext_hook__
|
||||||
if __unpack_ext_hook__ is not None
|
if __unpack_ext_hook__ is not None
|
||||||
@@ -87,35 +74,108 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
out["kwargs"] = kwargs
|
out["kwargs"] = kwargs
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def _default(self, obj: Any) -> str | dict[str, Any]:
|
||||||
|
if isinstance(obj, Serializable):
|
||||||
|
return cast(dict[str, Any], obj.to_json())
|
||||||
|
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
obj.__class__, method=(None, "model_construct"), kwargs=obj.model_dump()
|
||||||
|
)
|
||||||
|
elif hasattr(obj, "dict") and callable(obj.dict):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
obj.__class__, method=(None, "construct"), kwargs=obj.dict()
|
||||||
|
)
|
||||||
|
elif hasattr(obj, "_asdict") and callable(obj._asdict):
|
||||||
|
return self._encode_constructor_args(obj.__class__, kwargs=obj._asdict())
|
||||||
|
elif isinstance(obj, pathlib.Path):
|
||||||
|
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
|
||||||
|
elif isinstance(obj, re.Pattern):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
re.compile, args=(obj.pattern, obj.flags)
|
||||||
|
)
|
||||||
|
elif isinstance(obj, UUID):
|
||||||
|
return self._encode_constructor_args(UUID, args=(obj.hex,))
|
||||||
|
elif isinstance(obj, decimal.Decimal):
|
||||||
|
return self._encode_constructor_args(decimal.Decimal, args=(str(obj),))
|
||||||
|
elif isinstance(obj, (set, frozenset, deque)):
|
||||||
|
return self._encode_constructor_args(type(obj), args=(tuple(obj),))
|
||||||
|
elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
|
||||||
|
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
|
||||||
|
elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
|
||||||
|
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
|
||||||
|
|
||||||
|
elif isinstance(obj, datetime):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
datetime, method="fromisoformat", args=(obj.isoformat(),)
|
||||||
|
)
|
||||||
|
elif isinstance(obj, timezone):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
timezone,
|
||||||
|
args=obj.__getinitargs__(), # type: ignore[attr-defined]
|
||||||
|
)
|
||||||
|
elif isinstance(obj, ZoneInfo):
|
||||||
|
return self._encode_constructor_args(ZoneInfo, args=(obj.key,))
|
||||||
|
elif isinstance(obj, timedelta):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
timedelta, args=(obj.days, obj.seconds, obj.microseconds)
|
||||||
|
)
|
||||||
|
elif isinstance(obj, date):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
date, args=(obj.year, obj.month, obj.day)
|
||||||
|
)
|
||||||
|
elif isinstance(obj, time):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
time,
|
||||||
|
args=(obj.hour, obj.minute, obj.second, obj.microsecond, obj.tzinfo),
|
||||||
|
kwargs={"fold": obj.fold},
|
||||||
|
)
|
||||||
|
elif dataclasses.is_dataclass(obj):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
obj.__class__,
|
||||||
|
kwargs={
|
||||||
|
field.name: getattr(obj, field.name)
|
||||||
|
for field in dataclasses.fields(obj)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
elif isinstance(obj, Enum):
|
||||||
|
return self._encode_constructor_args(obj.__class__, args=(obj.value,))
|
||||||
|
elif isinstance(obj, SendProtocol):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
obj.__class__, kwargs={"node": obj.node, "arg": obj.arg}
|
||||||
|
)
|
||||||
|
elif isinstance(obj, (bytes, bytearray)):
|
||||||
|
return self._encode_constructor_args(
|
||||||
|
obj.__class__, method="fromhex", args=(obj.hex(),)
|
||||||
|
)
|
||||||
|
elif isinstance(obj, BaseException):
|
||||||
|
return repr(obj)
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
f"Object of type {obj.__class__.__name__} is not JSON serializable"
|
||||||
|
)
|
||||||
|
|
||||||
def _reviver(self, value: dict[str, Any]) -> Any:
|
def _reviver(self, value: dict[str, Any]) -> Any:
|
||||||
if self._allowed_modules and (
|
if (
|
||||||
value.get("lc", None) == 2
|
value.get("lc", None) == 2
|
||||||
and value.get("type", None) == "constructor"
|
and value.get("type", None) == "constructor"
|
||||||
and value.get("id", None) is not None
|
and value.get("id", None) is not None
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
return self._revive_lc2(value)
|
# Get module and class name
|
||||||
except InvalidModuleError as e:
|
|
||||||
logger.warning(
|
|
||||||
"Object %s is not in the deserialization allowlist.\n%s",
|
|
||||||
value["id"],
|
|
||||||
e.message,
|
|
||||||
)
|
|
||||||
|
|
||||||
return LC_REVIVER(value)
|
|
||||||
|
|
||||||
def _revive_lc2(self, value: dict[str, Any]) -> Any:
|
|
||||||
self._check_allowed_modules(value)
|
|
||||||
|
|
||||||
[*module, name] = value["id"]
|
[*module, name] = value["id"]
|
||||||
try:
|
# Import module
|
||||||
mod = importlib.import_module(".".join(module))
|
mod = importlib.import_module(".".join(module))
|
||||||
|
# Import class
|
||||||
cls = getattr(mod, name)
|
cls = getattr(mod, name)
|
||||||
|
# Instantiate class
|
||||||
method = value.get("method")
|
method = value.get("method")
|
||||||
if isinstance(method, str):
|
if isinstance(method, str):
|
||||||
methods = [getattr(cls, method)]
|
methods = [getattr(cls, method)]
|
||||||
elif isinstance(method, list):
|
elif isinstance(method, list):
|
||||||
methods = [cls if m is None else getattr(cls, m) for m in method]
|
methods = [
|
||||||
|
cls if method is None else getattr(cls, method)
|
||||||
|
for method in method
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
methods = [cls]
|
methods = [cls]
|
||||||
args = value.get("args")
|
args = value.get("args")
|
||||||
@@ -137,40 +197,11 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_allowed_modules(self, value: dict[str, Any]) -> None:
|
return LC_REVIVER(value)
|
||||||
needed = tuple(value["id"])
|
|
||||||
method = value.get("method")
|
|
||||||
if isinstance(method, list):
|
|
||||||
method_display = ",".join(m or "<init>" for m in method)
|
|
||||||
elif isinstance(method, str):
|
|
||||||
method_display = method
|
|
||||||
else:
|
|
||||||
method_display = "<init>"
|
|
||||||
|
|
||||||
dotted = ".".join(needed)
|
def dumps(self, obj: Any) -> bytes:
|
||||||
if not self._allowed_modules:
|
return json.dumps(obj, default=self._default, ensure_ascii=False).encode(
|
||||||
raise InvalidModuleError(
|
"utf-8", "ignore"
|
||||||
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
|
|
||||||
"No allowed_json_modules configured.\n\n"
|
|
||||||
"Unblock with ONE of:\n"
|
|
||||||
f" • JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
|
|
||||||
" • (DANGEROUS) JsonPlusSerializer(allowed_json_modules=True)\n\n"
|
|
||||||
"Note: Prefix allowlists are intentionally unsupported; prefer exact symbols "
|
|
||||||
"or plain-JSON representations revived without import-time side effects."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._allowed_modules is True:
|
|
||||||
return
|
|
||||||
if needed in self._allowed_modules:
|
|
||||||
return
|
|
||||||
|
|
||||||
raise InvalidModuleError(
|
|
||||||
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
|
|
||||||
"Symbol is not in the deserialization allowlist.\n\n"
|
|
||||||
"Add exactly this symbol to unblock:\n"
|
|
||||||
f" JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
|
|
||||||
"Or, as a last resort (DANGEROUS):\n"
|
|
||||||
" JsonPlusSerializer(allowed_json_modules=True)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||||
@@ -184,10 +215,15 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
try:
|
try:
|
||||||
return "msgpack", _msgpack_enc(obj)
|
return "msgpack", _msgpack_enc(obj)
|
||||||
except ormsgpack.MsgpackEncodeError as exc:
|
except ormsgpack.MsgpackEncodeError as exc:
|
||||||
if self.pickle_fallback:
|
if "valid UTF-8" in str(exc):
|
||||||
|
return "json", self.dumps(obj)
|
||||||
|
elif self.pickle_fallback:
|
||||||
return "pickle", pickle.dumps(obj)
|
return "pickle", pickle.dumps(obj)
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
def loads(self, data: bytes) -> Any:
|
||||||
|
return json.loads(data, object_hook=self._reviver)
|
||||||
|
|
||||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||||
type_, data_ = data
|
type_, data_ = data
|
||||||
if type_ == "null":
|
if type_ == "null":
|
||||||
@@ -197,7 +233,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
elif type_ == "bytearray":
|
elif type_ == "bytearray":
|
||||||
return bytearray(data_)
|
return bytearray(data_)
|
||||||
elif type_ == "json":
|
elif type_ == "json":
|
||||||
return json.loads(data_, object_hook=self._reviver)
|
return self.loads(data_)
|
||||||
elif type_ == "msgpack":
|
elif type_ == "msgpack":
|
||||||
return ormsgpack.unpackb(
|
return ormsgpack.unpackb(
|
||||||
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||||
@@ -627,20 +663,12 @@ def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class InvalidModuleError(Exception):
|
|
||||||
"""Exception raised when a module is not in the allowlist."""
|
|
||||||
|
|
||||||
def __init__(self, message: str):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
_option = (
|
_option = (
|
||||||
ormsgpack.OPT_NON_STR_KEYS
|
ormsgpack.OPT_NON_STR_KEYS
|
||||||
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
|
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
|
||||||
| ormsgpack.OPT_PASSTHROUGH_DATETIME
|
| ormsgpack.OPT_PASSTHROUGH_DATETIME
|
||||||
| ormsgpack.OPT_PASSTHROUGH_ENUM
|
| ormsgpack.OPT_PASSTHROUGH_ENUM
|
||||||
| ormsgpack.OPT_PASSTHROUGH_UUID
|
| ormsgpack.OPT_PASSTHROUGH_UUID
|
||||||
| ormsgpack.OPT_REPLACE_SURROGATES
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
Optional,
|
||||||
Protocol,
|
Protocol,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
runtime_checkable,
|
runtime_checkable,
|
||||||
@@ -27,9 +28,9 @@ class ChannelProtocol(Protocol[Value, Update, C]):
|
|||||||
@property
|
@property
|
||||||
def UpdateType(self) -> Any: ...
|
def UpdateType(self) -> Any: ...
|
||||||
|
|
||||||
def checkpoint(self) -> C | None: ...
|
def checkpoint(self) -> Optional[C]: ...
|
||||||
|
|
||||||
def from_checkpoint(self, checkpoint: C | None) -> Self: ...
|
def from_checkpoint(self, checkpoint: Optional[C]) -> Self: ...
|
||||||
|
|
||||||
def update(self, values: Sequence[Update]) -> bool: ...
|
def update(self, values: Sequence[Update]) -> bool: ...
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ Stores provide long-term memory that persists across threads and conversations.
|
|||||||
Supports hierarchical namespaces, key-value storage, and optional vector search.
|
Supports hierarchical namespaces, key-value storage, and optional vector search.
|
||||||
|
|
||||||
Core types:
|
Core types:
|
||||||
- `BaseStore`: Store interface with sync/async operations
|
- BaseStore: Store interface with sync/async operations
|
||||||
- `Item`: Stored key-value pairs with metadata
|
- Item: Stored key-value pairs with metadata
|
||||||
- `Op`: Get/Put/Search/List operations
|
- Op: Get/Put/Search/List operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -19,6 +19,7 @@ from typing import (
|
|||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
TypedDict,
|
TypedDict,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ class Item:
|
|||||||
key: Unique identifier within the namespace.
|
key: Unique identifier within the namespace.
|
||||||
namespace: Hierarchical path defining the collection in which this document resides.
|
namespace: Hierarchical path defining the collection in which this document resides.
|
||||||
Represented as a tuple of strings, allowing for nested categorization.
|
Represented as a tuple of strings, allowing for nested categorization.
|
||||||
For example: `("documents", 'user123')`
|
For example: ("documents", 'user123')
|
||||||
created_at: Timestamp of item creation.
|
created_at: Timestamp of item creation.
|
||||||
updated_at: Timestamp of last update.
|
updated_at: Timestamp of last update.
|
||||||
"""
|
"""
|
||||||
@@ -163,7 +164,6 @@ class GetOp(NamedTuple):
|
|||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Basic item retrieval:
|
Basic item retrieval:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
GetOp(namespace=("users", "profiles"), key="user123")
|
GetOp(namespace=("users", "profiles"), key="user123")
|
||||||
GetOp(namespace=("cache", "embeddings"), key="doc456")
|
GetOp(namespace=("cache", "embeddings"), key="doc456")
|
||||||
@@ -207,14 +207,11 @@ class SearchOp(NamedTuple):
|
|||||||
within a given namespace prefix. It provides pagination through limit and offset
|
within a given namespace prefix. It provides pagination through limit and offset
|
||||||
parameters.
|
parameters.
|
||||||
|
|
||||||
!!! note
|
Note:
|
||||||
|
|
||||||
Natural language search support depends on your store implementation.
|
Natural language search support depends on your store implementation.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Search with filters and pagination:
|
Search with filters and pagination:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
SearchOp(
|
SearchOp(
|
||||||
namespace_prefix=("documents",),
|
namespace_prefix=("documents",),
|
||||||
@@ -225,7 +222,6 @@ class SearchOp(NamedTuple):
|
|||||||
```
|
```
|
||||||
|
|
||||||
Natural language search:
|
Natural language search:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
SearchOp(
|
SearchOp(
|
||||||
namespace_prefix=("users", "content"),
|
namespace_prefix=("users", "content"),
|
||||||
@@ -253,15 +249,14 @@ class SearchOp(NamedTuple):
|
|||||||
The filter supports both exact matches and operator-based comparisons.
|
The filter supports both exact matches and operator-based comparisons.
|
||||||
|
|
||||||
Supported Operators:
|
Supported Operators:
|
||||||
- `$eq`: Equal to (same as direct value comparison)
|
- $eq: Equal to (same as direct value comparison)
|
||||||
- `$ne`: Not equal to
|
- $ne: Not equal to
|
||||||
- `$gt`: Greater than
|
- $gt: Greater than
|
||||||
- `$gte`: Greater than or equal to
|
- $gte: Greater than or equal to
|
||||||
- `$lt`: Less than
|
- $lt: Less than
|
||||||
- `$lte`: Less than or equal to
|
- $lte: Less than or equal to
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Simple exact match:
|
Simple exact match:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -294,7 +289,6 @@ class SearchOp(NamedTuple):
|
|||||||
"""Natural language search query for semantic search capabilities.
|
"""Natural language search query for semantic search capabilities.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
- "technical documentation about REST APIs"
|
- "technical documentation about REST APIs"
|
||||||
- "machine learning papers from 2023"
|
- "machine learning papers from 2023"
|
||||||
"""
|
"""
|
||||||
@@ -308,11 +302,10 @@ class SearchOp(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
# Type representing a namespace path that can include wildcards
|
# Type representing a namespace path that can include wildcards
|
||||||
NamespacePath = tuple[str | Literal["*"], ...]
|
NamespacePath = tuple[Union[str, Literal["*"]], ...]
|
||||||
"""A tuple representing a namespace path that can include wildcards.
|
"""A tuple representing a namespace path that can include wildcards.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
```python
|
```python
|
||||||
("users",) # Exact users namespace
|
("users",) # Exact users namespace
|
||||||
("documents", "*") # Any sub-namespace under documents
|
("documents", "*") # Any sub-namespace under documents
|
||||||
@@ -338,21 +331,17 @@ class MatchCondition(NamedTuple):
|
|||||||
hierarchies.
|
hierarchies.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Prefix matching:
|
Prefix matching:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
MatchCondition(match_type="prefix", path=("users", "profiles"))
|
MatchCondition(match_type="prefix", path=("users", "profiles"))
|
||||||
```
|
```
|
||||||
|
|
||||||
Suffix matching with wildcard:
|
Suffix matching with wildcard:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
MatchCondition(match_type="suffix", path=("cache", "*"))
|
MatchCondition(match_type="suffix", path=("cache", "*"))
|
||||||
```
|
```
|
||||||
|
|
||||||
Simple suffix matching:
|
Simple suffix matching:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
MatchCondition(match_type="suffix", path=("v1",))
|
MatchCondition(match_type="suffix", path=("v1",))
|
||||||
```
|
```
|
||||||
@@ -373,8 +362,7 @@ class ListNamespacesOp(NamedTuple):
|
|||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
List all namespaces under the `"documents"` path:
|
List all namespaces under the "documents" path:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ListNamespacesOp(
|
ListNamespacesOp(
|
||||||
match_conditions=(MatchCondition(match_type="prefix", path=("documents",)),),
|
match_conditions=(MatchCondition(match_type="prefix", path=("documents",)),),
|
||||||
@@ -382,8 +370,7 @@ class ListNamespacesOp(NamedTuple):
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
List all namespaces that end with `"v1"`:
|
List all namespaces that end with "v1":
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ListNamespacesOp(
|
ListNamespacesOp(
|
||||||
match_conditions=(MatchCondition(match_type="suffix", path=("v1",)),),
|
match_conditions=(MatchCondition(match_type="suffix", path=("v1",)),),
|
||||||
@@ -397,15 +384,12 @@ class ListNamespacesOp(NamedTuple):
|
|||||||
"""Optional conditions for filtering namespaces.
|
"""Optional conditions for filtering namespaces.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
All user namespaces:
|
All user namespaces:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
(MatchCondition(match_type="prefix", path=("users",)),)
|
(MatchCondition(match_type="prefix", path=("users",)),)
|
||||||
```
|
```
|
||||||
|
|
||||||
All namespaces that start with `"docs"` and end with `"draft"`:
|
All namespaces that start with "docs" and end with "draft":
|
||||||
|
|
||||||
```python
|
```python
|
||||||
(
|
(
|
||||||
MatchCondition(match_type="prefix", path=("docs",)),
|
MatchCondition(match_type="prefix", path=("docs",)),
|
||||||
@@ -442,21 +426,17 @@ class PutOp(NamedTuple):
|
|||||||
Each element in the tuple represents one level in the hierarchy.
|
Each element in the tuple represents one level in the hierarchy.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
Root level documents
|
||||||
Root level documents:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
("documents",)
|
("documents",)
|
||||||
```
|
```
|
||||||
|
|
||||||
User-specific documents:
|
User-specific documents
|
||||||
|
|
||||||
```python
|
```python
|
||||||
("documents", "user123")
|
("documents", "user123")
|
||||||
```
|
```
|
||||||
|
|
||||||
Nested cache structure:
|
Nested cache structure
|
||||||
|
|
||||||
```python
|
```python
|
||||||
("cache", "embeddings", "v1")
|
("cache", "embeddings", "v1")
|
||||||
```
|
```
|
||||||
@@ -469,15 +449,15 @@ class PutOp(NamedTuple):
|
|||||||
Together with the namespace, it forms a complete path to the item.
|
Together with the namespace, it forms a complete path to the item.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
If namespace is `("documents", "user123")` and key is `"report1"`,
|
If namespace is ("documents", "user123") and key is "report1",
|
||||||
the full path would effectively be `"documents/user123/report1"`
|
the full path would effectively be "documents/user123/report1"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value: dict[str, Any] | None
|
value: dict[str, Any] | None
|
||||||
"""The data to store, or `None` to mark the item for deletion.
|
"""The data to store, or None to mark the item for deletion.
|
||||||
|
|
||||||
The value must be a dictionary with string keys and JSON-serializable values.
|
The value must be a dictionary with string keys and JSON-serializable values.
|
||||||
Setting this to `None` signals that the item should be deleted.
|
Setting this to None signals that the item should be deleted.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
{
|
{
|
||||||
@@ -491,26 +471,25 @@ class PutOp(NamedTuple):
|
|||||||
"""Controls how the item's fields are indexed for search operations.
|
"""Controls how the item's fields are indexed for search operations.
|
||||||
|
|
||||||
Indexing configuration determines how the item can be found through search:
|
Indexing configuration determines how the item can be found through search:
|
||||||
- `None` (default): Uses the store's default indexing configuration (if provided)
|
- None (default): Uses the store's default indexing configuration (if provided)
|
||||||
- `False`: Disables indexing for this item
|
- False: Disables indexing for this item
|
||||||
- `list[str]`: Specifies which json path fields to index for search
|
- list[str]: Specifies which json path fields to index for search
|
||||||
|
|
||||||
The item remains accessible through direct get() operations regardless of indexing.
|
The item remains accessible through direct get() operations regardless of indexing.
|
||||||
When indexed, fields can be searched using natural language queries through
|
When indexed, fields can be searched using natural language queries through
|
||||||
vector similarity search (if supported by the store implementation).
|
vector similarity search (if supported by the store implementation).
|
||||||
|
|
||||||
Path Syntax:
|
Path Syntax:
|
||||||
- Simple field access: `"field"`
|
- Simple field access: "field"
|
||||||
- Nested fields: `"parent.child.grandchild"`
|
- Nested fields: "parent.child.grandchild"
|
||||||
- Array indexing:
|
- Array indexing:
|
||||||
- Specific index: `"array[0]"`
|
- Specific index: "array[0]"
|
||||||
- Last element: `"array[-1]"`
|
- Last element: "array[-1]"
|
||||||
- All elements (each individually): `"array[*]"`
|
- All elements (each individually): "array[*]"
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
- None - Use store defaults (whole item)
|
||||||
- `None` - Use store defaults (whole item)
|
- list[str] - List of fields to index
|
||||||
- `list[str]` - List of fields to index
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
[
|
[
|
||||||
@@ -530,12 +509,12 @@ class PutOp(NamedTuple):
|
|||||||
will expire this many minutes after it was last accessed. The expiration timer
|
will expire this many minutes after it was last accessed. The expiration timer
|
||||||
refreshes on both read operations (get/search) and write operations (put/update).
|
refreshes on both read operations (get/search) and write operations (put/update).
|
||||||
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
|
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
|
||||||
Defaults to `None` (no expiration).
|
Defaults to None (no expiration).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
Op = GetOp | SearchOp | PutOp | ListNamespacesOp
|
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
|
||||||
Result = Item | list[Item] | list[SearchItem] | list[tuple[str, ...]] | None
|
Result = Union[Item, list[Item], list[SearchItem], list[tuple[str, ...]], None]
|
||||||
|
|
||||||
|
|
||||||
class InvalidNamespaceError(ValueError):
|
class InvalidNamespaceError(ValueError):
|
||||||
@@ -546,18 +525,18 @@ class TTLConfig(TypedDict, total=False):
|
|||||||
"""Configuration for TTL (time-to-live) behavior in the store."""
|
"""Configuration for TTL (time-to-live) behavior in the store."""
|
||||||
|
|
||||||
refresh_on_read: bool
|
refresh_on_read: bool
|
||||||
"""Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
|
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
|
||||||
|
|
||||||
If `True`, TTLs will be refreshed on read operations (get/search) by default.
|
If True, TTLs will be refreshed on read operations (get/search) by default.
|
||||||
This can be overridden per-operation by explicitly setting `refresh_ttl`.
|
This can be overridden per-operation by explicitly setting refresh_ttl.
|
||||||
Defaults to `True` if not configured.
|
Defaults to True if not configured.
|
||||||
"""
|
"""
|
||||||
default_ttl: float | None
|
default_ttl: float | None
|
||||||
"""Default TTL (time-to-live) in minutes for new items.
|
"""Default TTL (time-to-live) in minutes for new items.
|
||||||
|
|
||||||
If provided, new items will expire after this many minutes after their last access.
|
If provided, new items will expire after this many minutes after their last access.
|
||||||
The expiration timer refreshes on both read and write operations.
|
The expiration timer refreshes on both read and write operations.
|
||||||
Defaults to `None` (no expiration).
|
Defaults to None (no expiration).
|
||||||
"""
|
"""
|
||||||
sweep_interval_minutes: int | None
|
sweep_interval_minutes: int | None
|
||||||
"""Interval in minutes between TTL sweep operations.
|
"""Interval in minutes between TTL sweep operations.
|
||||||
@@ -571,35 +550,33 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
"""Configuration for indexing documents for semantic search in the store.
|
"""Configuration for indexing documents for semantic search in the store.
|
||||||
|
|
||||||
If not provided to the store, the store will not support vector search.
|
If not provided to the store, the store will not support vector search.
|
||||||
In that case, all `index` arguments to `put()` and `aput()` operations will be ignored.
|
In that case, all `index` arguments to put() and `aput()` operations will be ignored.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dims: int
|
dims: int
|
||||||
"""Number of dimensions in the embedding vectors.
|
"""Number of dimensions in the embedding vectors.
|
||||||
|
|
||||||
Common embedding models have the following dimensions:
|
Common embedding models have the following dimensions:
|
||||||
- `openai:text-embedding-3-large`: `3072`
|
- openai:text-embedding-3-large: 3072
|
||||||
- `openai:text-embedding-3-small`: `1536`
|
- openai:text-embedding-3-small: 1536
|
||||||
- `openai:text-embedding-ada-002`: `1536`
|
- openai:text-embedding-ada-002: 1536
|
||||||
- `cohere:embed-english-v3.0`: `1024`
|
- cohere:embed-english-v3.0: 1024
|
||||||
- `cohere:embed-english-light-v3.0`: `384`
|
- cohere:embed-english-light-v3.0: 384
|
||||||
- `cohere:embed-multilingual-v3.0`: `1024`
|
- cohere:embed-multilingual-v3.0: 1024
|
||||||
- `cohere:embed-multilingual-light-v3.0`: `384`
|
- cohere:embed-multilingual-light-v3.0: 384
|
||||||
"""
|
"""
|
||||||
|
|
||||||
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
|
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
|
||||||
"""Optional function to generate embeddings from text.
|
"""Optional function to generate embeddings from text.
|
||||||
|
|
||||||
Can be specified in three ways:
|
Can be specified in three ways:
|
||||||
1. A LangChain `Embeddings` instance
|
1. A LangChain Embeddings instance
|
||||||
2. A synchronous embedding function (`EmbeddingsFunc`)
|
2. A synchronous embedding function (EmbeddingsFunc)
|
||||||
3. An asynchronous embedding function (`AEmbeddingsFunc`)
|
3. An asynchronous embedding function (AEmbeddingsFunc)
|
||||||
4. A provider string (e.g., `"openai:text-embedding-3-small"`)
|
4. A provider string (e.g., "openai:text-embedding-3-small")
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
Using LangChain's initialization with InMemoryStore:
|
||||||
Using LangChain's initialization with `InMemoryStore`:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from langchain.embeddings import init_embeddings
|
from langchain.embeddings import init_embeddings
|
||||||
from langgraph.store.memory import InMemoryStore
|
from langgraph.store.memory import InMemoryStore
|
||||||
@@ -612,8 +589,7 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Using a custom embedding function with `InMemoryStore`:
|
Using a custom embedding function with InMemoryStore:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from langgraph.store.memory import InMemoryStore
|
from langgraph.store.memory import InMemoryStore
|
||||||
@@ -635,8 +611,7 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Using an asynchronous embedding function with `InMemoryStore`:
|
Using an asynchronous embedding function with InMemoryStore:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
from langgraph.store.memory import InMemoryStore
|
from langgraph.store.memory import InMemoryStore
|
||||||
@@ -664,17 +639,16 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
|
|
||||||
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
|
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
|
||||||
|
|
||||||
- `["$"]`: Embeds the entire JSON object as one vector (default)
|
- ["$"]: Embeds the entire JSON object as one vector (default)
|
||||||
- `["field1", "field2"]`: Embeds specific top-level fields
|
- ["field1", "field2"]: Embeds specific top-level fields
|
||||||
- `["parent.child"]`: Embeds nested fields using dot notation
|
- ["parent.child"]: Embeds nested fields using dot notation
|
||||||
- `["array[*].field"]`: Embeds field from each array element separately
|
- ["array[*].field"]: Embeds field from each array element separately
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
You can always override this behavior when storing an item using the
|
You can always override this behavior when storing an item using the
|
||||||
`index` parameter in the `put` or `aput` operations.
|
`index` parameter in the `put` or `aput` operations.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Embed entire document (default)
|
# Embed entire document (default)
|
||||||
fields=["$"]
|
fields=["$"]
|
||||||
@@ -693,7 +667,7 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
Note:
|
Note:
|
||||||
- Fields missing from a document are skipped
|
- Fields missing from a document are skipped
|
||||||
- Array notation creates separate embeddings for each element
|
- Array notation creates separate embeddings for each element
|
||||||
- Complex nested paths are supported (e.g., `"a.b[*].c.d"`)
|
- Complex nested paths are supported (e.g., "a.b[*].c.d")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -758,11 +732,11 @@ class BaseStore(ABC):
|
|||||||
namespace: Hierarchical path for the item.
|
namespace: Hierarchical path for the item.
|
||||||
key: Unique identifier within the namespace.
|
key: Unique identifier within the namespace.
|
||||||
refresh_ttl: Whether to refresh TTLs for the returned item.
|
refresh_ttl: Whether to refresh TTLs for the returned item.
|
||||||
If `None`, uses the store's default `refresh_ttl` setting.
|
If None (default), uses the store's default refresh_ttl setting.
|
||||||
If no TTL is specified, this argument is ignored.
|
If no TTL is specified, this argument is ignored.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved item or `None` if not found.
|
The retrieved item or None if not found.
|
||||||
"""
|
"""
|
||||||
return self.batch(
|
return self.batch(
|
||||||
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
|
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
|
||||||
@@ -794,9 +768,7 @@ class BaseStore(ABC):
|
|||||||
List of items matching the search criteria.
|
List of items matching the search criteria.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Basic filtering:
|
Basic filtering:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Search for documents with specific metadata
|
# Search for documents with specific metadata
|
||||||
results = store.search(
|
results = store.search(
|
||||||
@@ -806,7 +778,6 @@ class BaseStore(ABC):
|
|||||||
```
|
```
|
||||||
|
|
||||||
Natural language search (requires vector store implementation):
|
Natural language search (requires vector store implementation):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Initialize store with embedding configuration
|
# Initialize store with embedding configuration
|
||||||
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
|
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
|
||||||
@@ -818,7 +789,6 @@ class BaseStore(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Search for semantically similar documents
|
# Search for semantically similar documents
|
||||||
|
|
||||||
results = store.search(
|
results = store.search(
|
||||||
("docs",),
|
("docs",),
|
||||||
query="machine learning applications in healthcare",
|
query="machine learning applications in healthcare",
|
||||||
@@ -827,9 +797,7 @@ class BaseStore(ABC):
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
Note: Natural language search support depends on your store implementation
|
||||||
|
|
||||||
Natural language search support depends on your store implementation
|
|
||||||
and requires proper embedding configuration.
|
and requires proper embedding configuration.
|
||||||
"""
|
"""
|
||||||
return self.batch(
|
return self.batch(
|
||||||
@@ -858,7 +826,7 @@ class BaseStore(ABC):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace: Hierarchical path for the item, represented as a tuple of strings.
|
namespace: Hierarchical path for the item, represented as a tuple of strings.
|
||||||
Example: `("documents", "user123")`
|
Example: ("documents", "user123")
|
||||||
key: Unique identifier within the namespace. Together with namespace forms
|
key: Unique identifier within the namespace. Together with namespace forms
|
||||||
the complete path to the item.
|
the complete path to the item.
|
||||||
value: Dictionary containing the item's data. Must contain string keys
|
value: Dictionary containing the item's data. Must contain string keys
|
||||||
@@ -869,10 +837,10 @@ class BaseStore(ABC):
|
|||||||
If you do not initialize the store with indexing capabilities,
|
If you do not initialize the store with indexing capabilities,
|
||||||
the `index` parameter will be ignored
|
the `index` parameter will be ignored
|
||||||
- False: Disable indexing for this item
|
- False: Disable indexing for this item
|
||||||
- `list[str]`: List of field paths to index, supporting:
|
- list[str]: List of field paths to index, supporting:
|
||||||
- Nested fields: `"metadata.title"`
|
- Nested fields: "metadata.title"
|
||||||
- Array access: `"chapters[*].content"` (each indexed separately)
|
- Array access: "chapters[*].content" (each indexed separately)
|
||||||
- Specific indices: `"authors[0].name"`
|
- Specific indices: "authors[0].name"
|
||||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||||
None means no expiration. Expired runs will be deleted opportunistically.
|
None means no expiration. Expired runs will be deleted opportunistically.
|
||||||
@@ -888,22 +856,18 @@ class BaseStore(ABC):
|
|||||||
Some implementations may not support expiration of items.
|
Some implementations may not support expiration of items.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
Store item. Indexing depends on how you configure the store.
|
||||||
Store item. Indexing depends on how you configure the store:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
store.put(("docs",), "report", {"memory": "Will likes ai"})
|
store.put(("docs",), "report", {"memory": "Will likes ai"})
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not index item for semantic search. Still accessible through `get()`
|
Do not index item for semantic search. Still accessible through get()
|
||||||
and `search()` operations but won't have a vector representation.
|
and search() operations but won't have a vector representation.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
|
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
Index specific fields for search:
|
Index specific fields for search.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
|
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
|
||||||
```
|
```
|
||||||
@@ -954,17 +918,15 @@ class BaseStore(ABC):
|
|||||||
suffix: Filter namespaces that end with this path.
|
suffix: Filter namespaces that end with this path.
|
||||||
max_depth: Return namespaces up to this depth in the hierarchy.
|
max_depth: Return namespaces up to this depth in the hierarchy.
|
||||||
Namespaces deeper than this level will be truncated.
|
Namespaces deeper than this level will be truncated.
|
||||||
limit: Maximum number of namespaces to return.
|
limit: Maximum number of namespaces to return (default 100).
|
||||||
offset: Number of namespaces to skip for pagination.
|
offset: Number of namespaces to skip for pagination (default 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of namespace tuples that match the criteria. Each tuple represents a
|
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
|
||||||
full namespace path up to `max_depth`.
|
Each tuple represents a full namespace path up to `max_depth`.
|
||||||
|
|
||||||
???+ example "Examples":
|
???+ example "Examples":
|
||||||
|
Setting max_depth=3. Given the namespaces:
|
||||||
Setting `max_depth=3`. Given the namespaces:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Example if you have the following namespaces:
|
# Example if you have the following namespaces:
|
||||||
# ("a", "b", "c")
|
# ("a", "b", "c")
|
||||||
@@ -1004,7 +966,7 @@ class BaseStore(ABC):
|
|||||||
key: Unique identifier within the namespace.
|
key: Unique identifier within the namespace.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The retrieved item or `None` if not found.
|
The retrieved item or None if not found.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
await self.abatch(
|
await self.abatch(
|
||||||
@@ -1038,16 +1000,14 @@ class BaseStore(ABC):
|
|||||||
limit: Maximum number of items to return.
|
limit: Maximum number of items to return.
|
||||||
offset: Number of items to skip before returning results.
|
offset: Number of items to skip before returning results.
|
||||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||||
If `None`, uses the store's `TTLConfig.refresh_default` setting.
|
If None (default), uses the store's TTLConfig.refresh_default setting.
|
||||||
If `TTLConfig` is not provided or no TTL is specified, this argument is ignored.
|
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of items matching the search criteria.
|
List of items matching the search criteria.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
|
||||||
Basic filtering:
|
Basic filtering:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Search for documents with specific metadata
|
# Search for documents with specific metadata
|
||||||
results = await store.asearch(
|
results = await store.asearch(
|
||||||
@@ -1057,7 +1017,6 @@ class BaseStore(ABC):
|
|||||||
```
|
```
|
||||||
|
|
||||||
Natural language search (requires vector store implementation):
|
Natural language search (requires vector store implementation):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Initialize store with embedding configuration
|
# Initialize store with embedding configuration
|
||||||
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
|
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
|
||||||
@@ -1069,7 +1028,6 @@ class BaseStore(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Search for semantically similar documents
|
# Search for semantically similar documents
|
||||||
|
|
||||||
results = await store.asearch(
|
results = await store.asearch(
|
||||||
("docs",),
|
("docs",),
|
||||||
query="machine learning applications in healthcare",
|
query="machine learning applications in healthcare",
|
||||||
@@ -1078,9 +1036,7 @@ class BaseStore(ABC):
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
Note: Natural language search support depends on your store implementation
|
||||||
|
|
||||||
Natural language search support depends on your store implementation
|
|
||||||
and requires proper embedding configuration.
|
and requires proper embedding configuration.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
@@ -1111,7 +1067,7 @@ class BaseStore(ABC):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace: Hierarchical path for the item, represented as a tuple of strings.
|
namespace: Hierarchical path for the item, represented as a tuple of strings.
|
||||||
Example: `("documents", "user123")`
|
Example: ("documents", "user123")
|
||||||
key: Unique identifier within the namespace. Together with namespace forms
|
key: Unique identifier within the namespace. Together with namespace forms
|
||||||
the complete path to the item.
|
the complete path to the item.
|
||||||
value: Dictionary containing the item's data. Must contain string keys
|
value: Dictionary containing the item's data. Must contain string keys
|
||||||
@@ -1122,10 +1078,10 @@ class BaseStore(ABC):
|
|||||||
If you do not initialize the store with indexing capabilities,
|
If you do not initialize the store with indexing capabilities,
|
||||||
the `index` parameter will be ignored
|
the `index` parameter will be ignored
|
||||||
- False: Disable indexing for this item
|
- False: Disable indexing for this item
|
||||||
- `list[str]`: List of field paths to index, supporting:
|
- list[str]: List of field paths to index, supporting:
|
||||||
- Nested fields: `"metadata.title"`
|
- Nested fields: "metadata.title"
|
||||||
- Array access: `"chapters[*].content"` (each indexed separately)
|
- Array access: "chapters[*].content" (each indexed separately)
|
||||||
- Specific indices: `"authors[0].name"`
|
- Specific indices: "authors[0].name"
|
||||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||||
None means no expiration. Expired runs will be deleted opportunistically.
|
None means no expiration. Expired runs will be deleted opportunistically.
|
||||||
@@ -1141,22 +1097,18 @@ class BaseStore(ABC):
|
|||||||
Some implementations may not support expiration of items.
|
Some implementations may not support expiration of items.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
Store item. Indexing depends on how you configure the store.
|
||||||
Store item. Indexing depends on how you configure the store:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
await store.aput(("docs",), "report", {"memory": "Will likes ai"})
|
await store.aput(("docs",), "report", {"memory": "Will likes ai"})
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not index item for semantic search. Still accessible through `get()`
|
Do not index item for semantic search. Still accessible through get()
|
||||||
and `search()` operations but won't have a vector representation.
|
and search() operations but won't have a vector representation.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
await store.aput(("docs",), "report", {"memory": "Will likes ai"}, index=False)
|
await store.aput(("docs",), "report", {"memory": "Will likes ai"}, index=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
Index specific fields for search (if store configured to index items):
|
Index specific fields for search (if store configured to index items):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
await store.aput(
|
await store.aput(
|
||||||
("docs",),
|
("docs",),
|
||||||
@@ -1215,16 +1167,15 @@ class BaseStore(ABC):
|
|||||||
suffix: Filter namespaces that end with this path.
|
suffix: Filter namespaces that end with this path.
|
||||||
max_depth: Return namespaces up to this depth in the hierarchy.
|
max_depth: Return namespaces up to this depth in the hierarchy.
|
||||||
Namespaces deeper than this level will be truncated to this depth.
|
Namespaces deeper than this level will be truncated to this depth.
|
||||||
limit: Maximum number of namespaces to return.
|
limit: Maximum number of namespaces to return (default 100).
|
||||||
offset: Number of namespaces to skip for pagination.
|
offset: Number of namespaces to skip for pagination (default 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of namespace tuples that match the criteria. Each tuple represents a
|
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
|
||||||
full namespace path up to `max_depth`.
|
Each tuple represents a full namespace path up to `max_depth`.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
|
Setting max_depth=3 with existing namespaces:
|
||||||
Setting `max_depth=3` with existing namespaces:
|
|
||||||
```python
|
```python
|
||||||
# Given the following namespaces:
|
# Given the following namespaces:
|
||||||
# ("a", "b", "c")
|
# ("a", "b", "c")
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import weakref
|
import weakref
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Iterable
|
||||||
from typing import Any, Literal, TypeVar
|
from typing import Any, Callable, Literal, TypeVar
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
NOT_PROVIDED,
|
NOT_PROVIDED,
|
||||||
@@ -349,7 +349,7 @@ async def _run(
|
|||||||
results = [results[ix] for ix in listen]
|
results = [results[ix] for ix in listen]
|
||||||
|
|
||||||
# set the results of each operation
|
# set the results of each operation
|
||||||
for fut, result in zip(futs, results, strict=False):
|
for fut, result in zip(futs, results):
|
||||||
# guard against future being done (e.g. cancelled)
|
# guard against future being done (e.g. cancelled)
|
||||||
if not fut.done():
|
if not fut.done():
|
||||||
fut.set_result(result)
|
fut.set_result(result)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
from collections.abc import Awaitable, Callable, Sequence
|
from collections.abc import Awaitable, Sequence
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
|
||||||
@@ -49,9 +49,7 @@ def ensure_embeddings(
|
|||||||
An Embeddings instance that wraps the provided function(s).
|
An Embeddings instance that wraps the provided function(s).
|
||||||
|
|
||||||
??? example "Examples"
|
??? example "Examples"
|
||||||
|
|
||||||
Wrap a synchronous embedding function:
|
Wrap a synchronous embedding function:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def my_embed_fn(texts):
|
def my_embed_fn(texts):
|
||||||
return [[0.1, 0.2] for _ in texts]
|
return [[0.1, 0.2] for _ in texts]
|
||||||
@@ -61,7 +59,6 @@ def ensure_embeddings(
|
|||||||
```
|
```
|
||||||
|
|
||||||
Wrap an asynchronous embedding function:
|
Wrap an asynchronous embedding function:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def my_async_fn(texts):
|
async def my_async_fn(texts):
|
||||||
return [[0.1, 0.2] for _ in texts]
|
return [[0.1, 0.2] for _ in texts]
|
||||||
@@ -71,7 +68,6 @@ def ensure_embeddings(
|
|||||||
```
|
```
|
||||||
|
|
||||||
Initialize embeddings using a provider string:
|
Initialize embeddings using a provider string:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
|
# Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
|
||||||
embeddings = ensure_embeddings("openai:text-embedding-3-small")
|
embeddings = ensure_embeddings("openai:text-embedding-3-small")
|
||||||
@@ -123,9 +119,7 @@ class EmbeddingsLambda(Embeddings):
|
|||||||
will raise an error. If sync, it will be used for both sync and async operations.
|
will raise an error. If sync, it will be used for both sync and async operations.
|
||||||
|
|
||||||
??? example "Examples"
|
??? example "Examples"
|
||||||
|
|
||||||
With a sync function:
|
With a sync function:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def my_embed_fn(texts):
|
def my_embed_fn(texts):
|
||||||
# Return 2D embeddings for each text
|
# Return 2D embeddings for each text
|
||||||
@@ -137,7 +131,6 @@ class EmbeddingsLambda(Embeddings):
|
|||||||
```
|
```
|
||||||
|
|
||||||
With an async function:
|
With an async function:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def my_async_fn(texts):
|
async def my_async_fn(texts):
|
||||||
return [[0.1, 0.2] for _ in texts]
|
return [[0.1, 0.2] for _ in texts]
|
||||||
@@ -245,7 +238,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
|||||||
- Nested paths in multi-field: "{field1,nested.field2}"
|
- Nested paths in multi-field: "{field1,nested.field2}"
|
||||||
"""
|
"""
|
||||||
if not path or path == "$":
|
if not path or path == "$":
|
||||||
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
|
return [json.dumps(obj, sort_keys=True)]
|
||||||
|
|
||||||
tokens = tokenize_path(path) if isinstance(path, str) else path
|
tokens = tokenize_path(path) if isinstance(path, str) else path
|
||||||
|
|
||||||
@@ -256,7 +249,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
|||||||
elif obj is None:
|
elif obj is None:
|
||||||
return []
|
return []
|
||||||
elif isinstance(obj, (list, dict)):
|
elif isinstance(obj, (list, dict)):
|
||||||
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
|
return [json.dumps(obj, sort_keys=True)]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
token = tokens[pos]
|
token = tokens[pos]
|
||||||
@@ -302,11 +295,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
|||||||
if isinstance(current_obj, (str, int, float, bool)):
|
if isinstance(current_obj, (str, int, float, bool)):
|
||||||
results.append(str(current_obj))
|
results.append(str(current_obj))
|
||||||
elif isinstance(current_obj, (list, dict)):
|
elif isinstance(current_obj, (list, dict)):
|
||||||
results.append(
|
results.append(json.dumps(current_obj, sort_keys=True))
|
||||||
json.dumps(
|
|
||||||
current_obj, sort_keys=True, ensure_ascii=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle wildcard
|
# Handle wildcard
|
||||||
elif token == "*":
|
elif token == "*":
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class InMemoryStore(BaseStore):
|
|||||||
if queries:
|
if queries:
|
||||||
coros = [self.embeddings.aembed_query(q) for q in list(queries)]
|
coros = [self.embeddings.aembed_query(q) for q in list(queries)]
|
||||||
results = await asyncio.gather(*coros)
|
results = await asyncio.gather(*coros)
|
||||||
queryinmem_store = dict(zip(queries, results, strict=False))
|
queryinmem_store = dict(zip(queries, results))
|
||||||
|
|
||||||
return queryinmem_store
|
return queryinmem_store
|
||||||
|
|
||||||
@@ -323,9 +323,7 @@ class InMemoryStore(BaseStore):
|
|||||||
|
|
||||||
scores = _cosine_similarity(query_embedding, flat_vectors)
|
scores = _cosine_similarity(query_embedding, flat_vectors)
|
||||||
sorted_results = sorted(
|
sorted_results = sorted(
|
||||||
zip(scores, flat_items, strict=False),
|
zip(scores, flat_items), key=lambda x: x[0], reverse=True
|
||||||
key=lambda x: x[0],
|
|
||||||
reverse=True,
|
|
||||||
)
|
)
|
||||||
# max pooling
|
# max pooling
|
||||||
seen: set[tuple[tuple[str, ...], str]] = set()
|
seen: set[tuple[tuple[str, ...], str]] = set()
|
||||||
@@ -454,7 +452,7 @@ class InMemoryStore(BaseStore):
|
|||||||
f"Number of embeddings ({len(embeddings)}) does not"
|
f"Number of embeddings ({len(embeddings)}) does not"
|
||||||
f" match number of indices ({len(indices)})"
|
f" match number of indices ({len(indices)})"
|
||||||
)
|
)
|
||||||
for embedding, (ns, key, path) in zip(embeddings, indices, strict=False):
|
for embedding, (ns, key, path) in zip(embeddings, indices):
|
||||||
self._vectors[ns][key][path] = embedding
|
self._vectors[ns][key][path] = embedding
|
||||||
|
|
||||||
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
||||||
@@ -513,7 +511,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
|||||||
|
|
||||||
similarities = []
|
similarities = []
|
||||||
for y in Y:
|
for y in Y:
|
||||||
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
dot_product = sum(a * b for a, b in zip(X, y))
|
||||||
norm1 = sum(a * a for a in X) ** 0.5
|
norm1 = sum(a * a for a in X) ** 0.5
|
||||||
norm2 = sum(a * a for a in y) ** 0.5
|
norm2 = sum(a * a for a in y) ** 0.5
|
||||||
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
||||||
@@ -531,14 +529,14 @@ def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if match_type == "prefix":
|
if match_type == "prefix":
|
||||||
for k_elem, p_elem in zip(key, path, strict=False):
|
for k_elem, p_elem in zip(key, path):
|
||||||
if p_elem == "*":
|
if p_elem == "*":
|
||||||
continue # Wildcard matches any element
|
continue # Wildcard matches any element
|
||||||
if k_elem != p_elem:
|
if k_elem != p_elem:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
elif match_type == "suffix":
|
elif match_type == "suffix":
|
||||||
for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):
|
for k_elem, p_elem in zip(reversed(key), reversed(path)):
|
||||||
if p_elem == "*":
|
if p_elem == "*":
|
||||||
continue # Wildcard matches any element
|
continue # Wildcard matches any element
|
||||||
if k_elem != p_elem:
|
if k_elem != p_elem:
|
||||||
@@ -565,10 +563,7 @@ def _compare_values(item_value: Any, filter_value: Any) -> bool:
|
|||||||
return (
|
return (
|
||||||
isinstance(item_value, (list, tuple))
|
isinstance(item_value, (list, tuple))
|
||||||
and len(item_value) == len(filter_value)
|
and len(item_value) == len(filter_value)
|
||||||
and all(
|
and all(_compare_values(iv, fv) for iv, fv in zip(item_value, filter_value))
|
||||||
_compare_values(iv, fv)
|
|
||||||
for iv, fv in zip(item_value, filter_value, strict=False)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return item_value == filter_value
|
return item_value == filter_value
|
||||||
|
|||||||
@@ -4,45 +4,36 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "langgraph-checkpoint"
|
name = "langgraph-checkpoint"
|
||||||
version = "3.0.1"
|
version = "2.1.1"
|
||||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||||
authors = []
|
authors = []
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.9"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ['LICENSE']
|
license-files = ['LICENSE']
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"langchain-core>=0.2.38",
|
"langchain-core>=0.2.38",
|
||||||
"ormsgpack>=1.12.0",
|
"ormsgpack>=1.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint"
|
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||||
Twitter = "https://x.com/LangChainAI"
|
|
||||||
Slack = "https://www.langchain.com/join-community"
|
|
||||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
test = [
|
dev = [
|
||||||
|
"ruff",
|
||||||
|
"codespell",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
"pytest-watcher",
|
"pytest-watcher",
|
||||||
|
"mypy",
|
||||||
"dataclasses-json",
|
"dataclasses-json",
|
||||||
"numpy",
|
"numpy",
|
||||||
"pandas",
|
"pandas",
|
||||||
"pandas-stubs>=2.2.2.240807",
|
"pandas-stubs>=2.2.2.240807",
|
||||||
"redis",
|
"redis",
|
||||||
]
|
]
|
||||||
lint = [
|
|
||||||
"ruff",
|
|
||||||
"codespell",
|
|
||||||
"mypy",
|
|
||||||
]
|
|
||||||
dev = [
|
|
||||||
{include-group = "test"},
|
|
||||||
{include-group = "lint"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
include = ["langgraph"]
|
include = ["langgraph"]
|
||||||
@@ -58,10 +49,8 @@ lint.select = [
|
|||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
"UP", # pyupgrade
|
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008"]
|
lint.ignore = ["E501", "B008"]
|
||||||
target-version = "py310"
|
|
||||||
|
|
||||||
[tool.pytest-watcher]
|
[tool.pytest-watcher]
|
||||||
now = true
|
now = true
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -20,7 +19,6 @@ from pydantic.v1 import BaseModel as BaseModelV1
|
|||||||
from pydantic.v1 import SecretStr as SecretStrV1
|
from pydantic.v1 import SecretStr as SecretStrV1
|
||||||
|
|
||||||
from langgraph.checkpoint.serde.jsonplus import (
|
from langgraph.checkpoint.serde.jsonplus import (
|
||||||
InvalidModuleError,
|
|
||||||
JsonPlusSerializer,
|
JsonPlusSerializer,
|
||||||
_msgpack_ext_hook_to_json,
|
_msgpack_ext_hook_to_json,
|
||||||
)
|
)
|
||||||
@@ -62,6 +60,13 @@ class MyDataclass:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
class MyDataclassWSlots(MyDataclass):
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
@dataclasses.dataclass(slots=True)
|
@dataclasses.dataclass(slots=True)
|
||||||
class MyDataclassWSlots:
|
class MyDataclassWSlots:
|
||||||
foo: str
|
foo: str
|
||||||
@@ -110,7 +115,11 @@ def test_serde_jsonplus() -> None:
|
|||||||
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
||||||
"my_enum": MyEnum.FOO,
|
"my_enum": MyEnum.FOO,
|
||||||
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
||||||
|
"my_pydantic_v1": MyPydanticV1(
|
||||||
|
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
||||||
|
),
|
||||||
"my_secret_str": SecretStr("meow"),
|
"my_secret_str": SecretStr("meow"),
|
||||||
|
"my_secret_str_v1": SecretStrV1("meow"),
|
||||||
"person": Person(name="foo"),
|
"person": Person(name="foo"),
|
||||||
"a_bool": True,
|
"a_bool": True,
|
||||||
"a_none": None,
|
"a_none": None,
|
||||||
@@ -132,12 +141,6 @@ def test_serde_jsonplus() -> None:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if sys.version_info < (3, 14):
|
|
||||||
to_serialize["my_pydantic_v1"] = MyPydanticV1(
|
|
||||||
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
|
||||||
)
|
|
||||||
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
|
|
||||||
|
|
||||||
serde = JsonPlusSerializer()
|
serde = JsonPlusSerializer()
|
||||||
|
|
||||||
dumped = serde.dumps_typed(to_serialize)
|
dumped = serde.dumps_typed(to_serialize)
|
||||||
@@ -149,22 +152,23 @@ def test_serde_jsonplus() -> None:
|
|||||||
assert serde.loads_typed(serde.dumps_typed(value)) == value
|
assert serde.loads_typed(serde.dumps_typed(value)) == value
|
||||||
|
|
||||||
surrogates = [
|
surrogates = [
|
||||||
"Hello??",
|
"Hello\ud83d\ude00",
|
||||||
"Python??",
|
"Python\ud83d\udc0d",
|
||||||
"Surrogate??",
|
"Surrogate\ud834\udd1e",
|
||||||
"Example??",
|
"Example\ud83c\udf89",
|
||||||
"String??",
|
"String\ud83c\udfa7",
|
||||||
"With??",
|
"With\ud83c\udf08",
|
||||||
"Surrogates??",
|
"Surrogates\ud83d\ude0e",
|
||||||
"Embedded??",
|
"Embedded\ud83d\udcbb",
|
||||||
"In??",
|
"In\ud83c\udf0e",
|
||||||
"The??",
|
"The\ud83d\udcd6",
|
||||||
"Text??",
|
"Text\ud83d\udcac",
|
||||||
"收花🙄·到",
|
"收花🙄·到",
|
||||||
]
|
]
|
||||||
serde = JsonPlusSerializer(pickle_fallback=False)
|
|
||||||
|
|
||||||
assert serde.loads_typed(serde.dumps_typed(surrogates)) == surrogates
|
assert serde.loads_typed(serde.dumps_typed(surrogates)) == [
|
||||||
|
v.encode("utf-8", "ignore").decode() for v in surrogates
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_serde_jsonplus_json_mode() -> None:
|
def test_serde_jsonplus_json_mode() -> None:
|
||||||
@@ -193,7 +197,11 @@ def test_serde_jsonplus_json_mode() -> None:
|
|||||||
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
||||||
"my_enum": MyEnum.FOO,
|
"my_enum": MyEnum.FOO,
|
||||||
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
||||||
|
"my_pydantic_v1": MyPydanticV1(
|
||||||
|
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
||||||
|
),
|
||||||
"my_secret_str": SecretStr("meow"),
|
"my_secret_str": SecretStr("meow"),
|
||||||
|
"my_secret_str_v1": SecretStrV1("meow"),
|
||||||
"person": Person(name="foo"),
|
"person": Person(name="foo"),
|
||||||
"a_bool": True,
|
"a_bool": True,
|
||||||
"a_none": None,
|
"a_none": None,
|
||||||
@@ -215,20 +223,13 @@ def test_serde_jsonplus_json_mode() -> None:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if sys.version_info < (3, 14):
|
|
||||||
to_serialize["my_pydantic_v1"] = MyPydanticV1(
|
|
||||||
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
|
||||||
)
|
|
||||||
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
|
|
||||||
|
|
||||||
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
|
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
|
||||||
|
|
||||||
dumped = serde.dumps_typed(to_serialize)
|
dumped = serde.dumps_typed(to_serialize)
|
||||||
|
|
||||||
assert dumped[0] == "msgpack"
|
assert dumped[0] == "msgpack"
|
||||||
result = serde.loads_typed(dumped)
|
result = serde.loads_typed(dumped)
|
||||||
|
assert result == {
|
||||||
expected_result = {
|
|
||||||
"path": ["foo", "bar"],
|
"path": ["foo", "bar"],
|
||||||
"re": ["foo", 48],
|
"re": ["foo", 48],
|
||||||
"decimal": "1.10101",
|
"decimal": "1.10101",
|
||||||
@@ -252,7 +253,9 @@ def test_serde_jsonplus_json_mode() -> None:
|
|||||||
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||||
"my_enum": "foo",
|
"my_enum": "foo",
|
||||||
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||||
|
"my_pydantic_v1": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||||
"my_secret_str": "meow",
|
"my_secret_str": "meow",
|
||||||
|
"my_secret_str_v1": "meow",
|
||||||
"person": {"name": "foo"},
|
"person": {"name": "foo"},
|
||||||
"a_bool": True,
|
"a_bool": True,
|
||||||
"a_none": None,
|
"a_none": None,
|
||||||
@@ -274,16 +277,6 @@ def test_serde_jsonplus_json_mode() -> None:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if sys.version_info < (3, 14):
|
|
||||||
expected_result["my_pydantic_v1"] = {
|
|
||||||
"foo": "foo",
|
|
||||||
"bar": 1,
|
|
||||||
"inner": {"hello": "hello"},
|
|
||||||
}
|
|
||||||
expected_result["my_secret_str_v1"] = "meow"
|
|
||||||
|
|
||||||
assert result == expected_result
|
|
||||||
|
|
||||||
|
|
||||||
def test_serde_jsonplus_bytes() -> None:
|
def test_serde_jsonplus_bytes() -> None:
|
||||||
serde = JsonPlusSerializer()
|
serde = JsonPlusSerializer()
|
||||||
@@ -295,20 +288,6 @@ def test_serde_jsonplus_bytes() -> None:
|
|||||||
assert serde.loads_typed(dumped) == some_bytes
|
assert serde.loads_typed(dumped) == some_bytes
|
||||||
|
|
||||||
|
|
||||||
def test_deserde_invalid_module() -> None:
|
|
||||||
serde = JsonPlusSerializer()
|
|
||||||
load = {
|
|
||||||
"lc": 2,
|
|
||||||
"type": "constructor",
|
|
||||||
"id": ["pprint", "pprint"],
|
|
||||||
"kwargs": {"object": "HELLO"},
|
|
||||||
}
|
|
||||||
with pytest.raises(InvalidModuleError):
|
|
||||||
serde._revive_lc2(load)
|
|
||||||
serde = JsonPlusSerializer(allowed_json_modules=[("pprint", "pprint")])
|
|
||||||
serde.loads_typed(("json", json.dumps(load).encode("utf-8")))
|
|
||||||
|
|
||||||
|
|
||||||
def test_serde_jsonplus_bytearray() -> None:
|
def test_serde_jsonplus_bytearray() -> None:
|
||||||
serde = JsonPlusSerializer()
|
serde = JsonPlusSerializer()
|
||||||
|
|
||||||
@@ -385,12 +364,7 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
|
|||||||
"str_col": ["a", None, "c"],
|
"str_col": ["a", None, "c"],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
pytest.param(
|
|
||||||
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
|
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
|
||||||
marks=pytest.mark.skipif(
|
|
||||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pd.DataFrame(
|
pd.DataFrame(
|
||||||
{
|
{
|
||||||
"int8": pd.array([1, 2, 3], dtype="int8"),
|
"int8": pd.array([1, 2, 3], dtype="int8"),
|
||||||
@@ -418,25 +392,11 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
|
|||||||
"col3": np.random.rand(1000),
|
"col3": np.random.rand(1000),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
pytest.param(
|
|
||||||
pd.DataFrame(
|
pd.DataFrame(
|
||||||
{
|
{"tz_datetime": pd.date_range("2024-01-01", periods=3, freq="D", tz="UTC")}
|
||||||
"tz_datetime": pd.date_range(
|
|
||||||
"2024-01-01", periods=3, freq="D", tz="UTC"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
marks=pytest.mark.skipif(
|
|
||||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
|
pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
|
||||||
pytest.param(
|
|
||||||
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
|
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
|
||||||
marks=pytest.mark.skipif(
|
|
||||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pd.DataFrame({"interval": pd.interval_range(start=0, end=3, periods=3)}),
|
pd.DataFrame({"interval": pd.interval_range(start=0, end=3, periods=3)}),
|
||||||
pd.DataFrame({"unicode": ["Hello 🌍", "Python 🐍", "Data 📊"]}),
|
pd.DataFrame({"unicode": ["Hello 🌍", "Python 🐍", "Data 📊"]}),
|
||||||
pd.DataFrame({"mixed": [1, "string", [1, 2, 3], {"key": "value"}]}),
|
pd.DataFrame({"mixed": [1, "string", [1, 2, 3], {"key": "value"}]}),
|
||||||
@@ -473,12 +433,7 @@ def test_serde_jsonplus_pandas_dataframe(df: pd.DataFrame) -> None:
|
|||||||
pd.Series([1, 2, None]),
|
pd.Series([1, 2, None]),
|
||||||
pd.Series([1.1, None, 3.3]),
|
pd.Series([1.1, None, 3.3]),
|
||||||
pd.Series(["a", None, "c"]),
|
pd.Series(["a", None, "c"]),
|
||||||
pytest.param(
|
|
||||||
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
|
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
|
||||||
marks=pytest.mark.skipif(
|
|
||||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pd.Series([1, 2, 3], dtype="int8"),
|
pd.Series([1, 2, 3], dtype="int8"),
|
||||||
pd.Series([10, 20, 30], dtype="int16"),
|
pd.Series([10, 20, 30], dtype="int16"),
|
||||||
pd.Series([100, 200, 300], dtype="int32"),
|
pd.Series([100, 200, 300], dtype="int32"),
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import time
|
|||||||
import pytest
|
import pytest
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
from langgraph.cache.base import FullKey
|
|
||||||
from langgraph.cache.redis import RedisCache
|
from langgraph.cache.redis import RedisCache
|
||||||
|
|
||||||
|
|
||||||
class TestRedisCache:
|
class TestRedisCache:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self) -> None:
|
def setup(self):
|
||||||
"""Set up test Redis client and cache."""
|
"""Set up test Redis client and cache."""
|
||||||
self.client = redis.Redis(
|
self.client = redis.Redis(
|
||||||
host="localhost", port=6379, db=0, decode_responses=False
|
host="localhost", port=6379, db=0, decode_responses=False
|
||||||
@@ -21,21 +20,21 @@ class TestRedisCache:
|
|||||||
except redis.ConnectionError:
|
except redis.ConnectionError:
|
||||||
pytest.skip("Redis server not available")
|
pytest.skip("Redis server not available")
|
||||||
|
|
||||||
self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:")
|
self.cache = RedisCache(self.client, prefix="test:cache:")
|
||||||
|
|
||||||
# Clean up before each test
|
# Clean up before each test
|
||||||
self.client.flushdb()
|
self.client.flushdb()
|
||||||
|
|
||||||
def teardown_method(self) -> None:
|
def teardown_method(self):
|
||||||
"""Clean up after each test."""
|
"""Clean up after each test."""
|
||||||
try:
|
try:
|
||||||
self.client.flushdb()
|
self.client.flushdb()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_basic_set_and_get(self) -> None:
|
def test_basic_set_and_get(self):
|
||||||
"""Test basic set and get operations."""
|
"""Test basic set and get operations."""
|
||||||
keys: list[FullKey] = [(("graph", "node"), "key1")]
|
keys = [(("graph", "node"), "key1")]
|
||||||
values = {keys[0]: ({"result": 42}, None)}
|
values = {keys[0]: ({"result": 42}, None)}
|
||||||
|
|
||||||
# Set value
|
# Set value
|
||||||
@@ -46,9 +45,9 @@ class TestRedisCache:
|
|||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[keys[0]] == {"result": 42}
|
assert result[keys[0]] == {"result": 42}
|
||||||
|
|
||||||
def test_batch_operations(self) -> None:
|
def test_batch_operations(self):
|
||||||
"""Test batch set and get operations."""
|
"""Test batch set and get operations."""
|
||||||
keys: list[FullKey] = [
|
keys = [
|
||||||
(("graph", "node1"), "key1"),
|
(("graph", "node1"), "key1"),
|
||||||
(("graph", "node2"), "key2"),
|
(("graph", "node2"), "key2"),
|
||||||
(("other", "node"), "key3"),
|
(("other", "node"), "key3"),
|
||||||
@@ -69,9 +68,9 @@ class TestRedisCache:
|
|||||||
assert result[keys[1]] == {"result": 2}
|
assert result[keys[1]] == {"result": 2}
|
||||||
assert result[keys[2]] == {"result": 3}
|
assert result[keys[2]] == {"result": 3}
|
||||||
|
|
||||||
def test_ttl_behavior(self) -> None:
|
def test_ttl_behavior(self):
|
||||||
"""Test TTL (time-to-live) functionality."""
|
"""Test TTL (time-to-live) functionality."""
|
||||||
key: FullKey = (("graph", "node"), "ttl_key")
|
key = (("graph", "node"), "ttl_key")
|
||||||
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
|
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
|
||||||
|
|
||||||
# Set with TTL
|
# Set with TTL
|
||||||
@@ -89,10 +88,10 @@ class TestRedisCache:
|
|||||||
result = self.cache.get([key])
|
result = self.cache.get([key])
|
||||||
assert len(result) == 0
|
assert len(result) == 0
|
||||||
|
|
||||||
def test_namespace_isolation(self) -> None:
|
def test_namespace_isolation(self):
|
||||||
"""Test that different namespaces are isolated."""
|
"""Test that different namespaces are isolated."""
|
||||||
key1: FullKey = (("graph1", "node"), "same_key")
|
key1 = (("graph1", "node"), "same_key")
|
||||||
key2: FullKey = (("graph2", "node"), "same_key")
|
key2 = (("graph2", "node"), "same_key")
|
||||||
|
|
||||||
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
|
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
|
||||||
|
|
||||||
@@ -102,12 +101,9 @@ class TestRedisCache:
|
|||||||
assert result[key1] == {"graph": 1}
|
assert result[key1] == {"graph": 1}
|
||||||
assert result[key2] == {"graph": 2}
|
assert result[key2] == {"graph": 2}
|
||||||
|
|
||||||
def test_clear_all(self) -> None:
|
def test_clear_all(self):
|
||||||
"""Test clearing all cached values."""
|
"""Test clearing all cached values."""
|
||||||
keys: list[FullKey] = [
|
keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
|
||||||
(("graph", "node1"), "key1"),
|
|
||||||
(("graph", "node2"), "key2"),
|
|
||||||
]
|
|
||||||
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
|
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
|
||||||
|
|
||||||
self.cache.set(values)
|
self.cache.set(values)
|
||||||
@@ -123,9 +119,9 @@ class TestRedisCache:
|
|||||||
result = self.cache.get(keys)
|
result = self.cache.get(keys)
|
||||||
assert len(result) == 0
|
assert len(result) == 0
|
||||||
|
|
||||||
def test_clear_by_namespace(self) -> None:
|
def test_clear_by_namespace(self):
|
||||||
"""Test clearing cached values by namespace."""
|
"""Test clearing cached values by namespace."""
|
||||||
keys: list[FullKey] = [
|
keys = [
|
||||||
(("graph1", "node"), "key1"),
|
(("graph1", "node"), "key1"),
|
||||||
(("graph2", "node"), "key2"),
|
(("graph2", "node"), "key2"),
|
||||||
(("graph1", "other"), "key3"),
|
(("graph1", "other"), "key3"),
|
||||||
@@ -146,7 +142,7 @@ class TestRedisCache:
|
|||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[keys[1]] == {"result": 2}
|
assert result[keys[1]] == {"result": 2}
|
||||||
|
|
||||||
def test_empty_operations(self) -> None:
|
def test_empty_operations(self):
|
||||||
"""Test behavior with empty keys/values."""
|
"""Test behavior with empty keys/values."""
|
||||||
# Empty get
|
# Empty get
|
||||||
result = self.cache.get([])
|
result = self.cache.get([])
|
||||||
@@ -155,14 +151,14 @@ class TestRedisCache:
|
|||||||
# Empty set
|
# Empty set
|
||||||
self.cache.set({}) # Should not raise error
|
self.cache.set({}) # Should not raise error
|
||||||
|
|
||||||
def test_nonexistent_keys(self) -> None:
|
def test_nonexistent_keys(self):
|
||||||
"""Test getting keys that don't exist."""
|
"""Test getting keys that don't exist."""
|
||||||
keys: list[FullKey] = [(("graph", "node"), "nonexistent")]
|
keys = [(("graph", "node"), "nonexistent")]
|
||||||
result = self.cache.get(keys)
|
result = self.cache.get(keys)
|
||||||
assert len(result) == 0
|
assert len(result) == 0
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_async_operations(self) -> None:
|
async def test_async_operations(self):
|
||||||
"""Test async set and get operations with sync Redis client."""
|
"""Test async set and get operations with sync Redis client."""
|
||||||
# Create sync Redis client and cache (like main integration tests)
|
# Create sync Redis client and cache (like main integration tests)
|
||||||
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
||||||
@@ -171,9 +167,9 @@ class TestRedisCache:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pytest.skip("Redis not available")
|
pytest.skip("Redis not available")
|
||||||
|
|
||||||
cache: RedisCache = RedisCache(client, prefix="test:async:")
|
cache = RedisCache(client, prefix="test:async:")
|
||||||
|
|
||||||
keys: list[FullKey] = [(("graph", "node"), "async_key")]
|
keys = [(("graph", "node"), "async_key")]
|
||||||
values = {keys[0]: ({"async": True}, None)}
|
values = {keys[0]: ({"async": True}, None)}
|
||||||
|
|
||||||
# Async set (delegates to sync)
|
# Async set (delegates to sync)
|
||||||
@@ -188,7 +184,7 @@ class TestRedisCache:
|
|||||||
client.flushdb()
|
client.flushdb()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_async_clear(self) -> None:
|
async def test_async_clear(self):
|
||||||
"""Test async clear operations with sync Redis client."""
|
"""Test async clear operations with sync Redis client."""
|
||||||
# Create sync Redis client and cache (like main integration tests)
|
# Create sync Redis client and cache (like main integration tests)
|
||||||
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
|
||||||
@@ -197,9 +193,9 @@ class TestRedisCache:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pytest.skip("Redis not available")
|
pytest.skip("Redis not available")
|
||||||
|
|
||||||
cache: RedisCache = RedisCache(client, prefix="test:async:")
|
cache = RedisCache(client, prefix="test:async:")
|
||||||
|
|
||||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
keys = [(("graph", "node"), "key")]
|
||||||
values = {keys[0]: ({"data": "test"}, None)}
|
values = {keys[0]: ({"data": "test"}, None)}
|
||||||
|
|
||||||
await cache.aset(values)
|
await cache.aset(values)
|
||||||
@@ -218,44 +214,44 @@ class TestRedisCache:
|
|||||||
# Cleanup
|
# Cleanup
|
||||||
client.flushdb()
|
client.flushdb()
|
||||||
|
|
||||||
def test_redis_unavailable_get(self) -> None:
|
def test_redis_unavailable_get(self):
|
||||||
"""Test behavior when Redis is unavailable during get operations."""
|
"""Test behavior when Redis is unavailable during get operations."""
|
||||||
# Create cache with non-existent Redis server
|
# Create cache with non-existent Redis server
|
||||||
bad_client = redis.Redis(
|
bad_client = redis.Redis(
|
||||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||||
)
|
)
|
||||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||||
|
|
||||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
keys = [(("graph", "node"), "key")]
|
||||||
result = cache.get(keys)
|
result = cache.get(keys)
|
||||||
|
|
||||||
# Should return empty dict when Redis unavailable
|
# Should return empty dict when Redis unavailable
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
def test_redis_unavailable_set(self) -> None:
|
def test_redis_unavailable_set(self):
|
||||||
"""Test behavior when Redis is unavailable during set operations."""
|
"""Test behavior when Redis is unavailable during set operations."""
|
||||||
# Create cache with non-existent Redis server
|
# Create cache with non-existent Redis server
|
||||||
bad_client = redis.Redis(
|
bad_client = redis.Redis(
|
||||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||||
)
|
)
|
||||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||||
|
|
||||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
keys = [(("graph", "node"), "key")]
|
||||||
values = {keys[0]: ({"data": "test"}, None)}
|
values = {keys[0]: ({"data": "test"}, None)}
|
||||||
|
|
||||||
# Should not raise exception when Redis unavailable
|
# Should not raise exception when Redis unavailable
|
||||||
cache.set(values) # Should silently fail
|
cache.set(values) # Should silently fail
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_redis_unavailable_async(self) -> None:
|
async def test_redis_unavailable_async(self):
|
||||||
"""Test async behavior when Redis is unavailable."""
|
"""Test async behavior when Redis is unavailable."""
|
||||||
# Create sync cache with non-existent Redis server (like main integration tests)
|
# Create sync cache with non-existent Redis server (like main integration tests)
|
||||||
bad_client = redis.Redis(
|
bad_client = redis.Redis(
|
||||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||||
)
|
)
|
||||||
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
|
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||||
|
|
||||||
keys: list[FullKey] = [(("graph", "node"), "key")]
|
keys = [(("graph", "node"), "key")]
|
||||||
values = {keys[0]: ({"data": "test"}, None)}
|
values = {keys[0]: ({"data": "test"}, None)}
|
||||||
|
|
||||||
# Should return empty dict for get (delegates to sync)
|
# Should return empty dict for get (delegates to sync)
|
||||||
@@ -265,10 +261,10 @@ class TestRedisCache:
|
|||||||
# Should not raise exception for set (delegates to sync)
|
# Should not raise exception for set (delegates to sync)
|
||||||
await cache.aset(values) # Should silently fail
|
await cache.aset(values) # Should silently fail
|
||||||
|
|
||||||
def test_corrupted_data_handling(self) -> None:
|
def test_corrupted_data_handling(self):
|
||||||
"""Test handling of corrupted data in Redis."""
|
"""Test handling of corrupted data in Redis."""
|
||||||
# Set some valid data first
|
# Set some valid data first
|
||||||
keys: list[FullKey] = [(("graph", "node"), "valid_key")]
|
keys = [(("graph", "node"), "valid_key")]
|
||||||
values = {keys[0]: ({"data": "valid"}, None)}
|
values = {keys[0]: ({"data": "valid"}, None)}
|
||||||
self.cache.set(values)
|
self.cache.set(values)
|
||||||
|
|
||||||
@@ -277,36 +273,33 @@ class TestRedisCache:
|
|||||||
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
|
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
|
||||||
|
|
||||||
# Should skip corrupted entry and return only valid ones
|
# Should skip corrupted entry and return only valid ones
|
||||||
all_keys: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")]
|
all_keys = [keys[0], (("graph", "node"), "corrupted_key")]
|
||||||
result = self.cache.get(all_keys)
|
result = self.cache.get(all_keys)
|
||||||
|
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[keys[0]] == {"data": "valid"}
|
assert result[keys[0]] == {"data": "valid"}
|
||||||
|
|
||||||
def test_key_parsing_edge_cases(self) -> None:
|
def test_key_parsing_edge_cases(self):
|
||||||
"""Test key parsing with edge cases."""
|
"""Test key parsing with edge cases."""
|
||||||
# Test empty namespace
|
# Test empty namespace
|
||||||
key1: FullKey = ((), "empty_ns")
|
key1 = ((), "empty_ns")
|
||||||
values = {key1: ({"data": "empty_ns"}, None)}
|
values = {key1: ({"data": "empty_ns"}, None)}
|
||||||
self.cache.set(values)
|
self.cache.set(values)
|
||||||
result = self.cache.get([key1])
|
result = self.cache.get([key1])
|
||||||
assert result[key1] == {"data": "empty_ns"}
|
assert result[key1] == {"data": "empty_ns"}
|
||||||
|
|
||||||
# Test namespace with special characters
|
# Test namespace with special characters
|
||||||
key2: FullKey = (
|
key2 = (("graph:with:colons", "node-with-dashes"), "key_with_underscores")
|
||||||
("graph:with:colons", "node-with-dashes"),
|
|
||||||
"key_with_underscores",
|
|
||||||
)
|
|
||||||
values = {key2: ({"data": "special_chars"}, None)}
|
values = {key2: ({"data": "special_chars"}, None)}
|
||||||
self.cache.set(values)
|
self.cache.set(values)
|
||||||
result = self.cache.get([key2])
|
result = self.cache.get([key2])
|
||||||
assert result[key2] == {"data": "special_chars"}
|
assert result[key2] == {"data": "special_chars"}
|
||||||
|
|
||||||
def test_large_data_serialization(self) -> None:
|
def test_large_data_serialization(self):
|
||||||
"""Test handling of large data objects."""
|
"""Test handling of large data objects."""
|
||||||
# Create a large data structure
|
# Create a large data structure
|
||||||
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
|
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
|
||||||
key: FullKey = (("graph", "node"), "large_key")
|
key = (("graph", "node"), "large_key")
|
||||||
values = {key: (large_data, None)}
|
values = {key: (large_data, None)}
|
||||||
|
|
||||||
self.cache.set(values)
|
self.cache.set(values)
|
||||||
|
|||||||
@@ -845,7 +845,7 @@ async def test_async_batched_vector_search_concurrent(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for results, (query, filter_) in zip(all_results, search_queries, strict=False):
|
for results, (query, filter_) in zip(all_results, search_queries):
|
||||||
assert len(results) > 0, f"No results for query '{query}' with filter {filter_}"
|
assert len(results) > 0, f"No results for query '{query}' with filter {filter_}"
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
@@ -950,8 +950,8 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
|
|||||||
assert results[0].key != results[1].key
|
assert results[0].key != results[1].key
|
||||||
ascore = results[0].score
|
ascore = results[0].score
|
||||||
bscore = results[1].score
|
bscore = results[1].score
|
||||||
|
assert ascore == bscore
|
||||||
assert ascore is not None and bscore is not None
|
assert ascore is not None and bscore is not None
|
||||||
assert ascore == pytest.approx(bscore, abs=1e-5)
|
|
||||||
|
|
||||||
results = await store.asearch(("test",), query="uuu")
|
results = await store.asearch(("test",), query="uuu")
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
@@ -1021,27 +1021,3 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
|
|||||||
assert len(results) == 3
|
assert len(results) == 3
|
||||||
doc5_result = next(r for r in results if r.key == "doc5")
|
doc5_result = next(r for r in results if r.key == "doc5")
|
||||||
assert doc5_result.score is None
|
assert doc5_result.score is None
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii(fake_embeddings: CharacterEmbeddings) -> None:
|
|
||||||
"""Test support for non-ascii characters"""
|
|
||||||
store = InMemoryStore(
|
|
||||||
index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
|
|
||||||
)
|
|
||||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
|
||||||
store.put(("user_123", "memories"), "2", {"text": "これは日本語です"}) # Japanese
|
|
||||||
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
|
||||||
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
|
||||||
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
|
||||||
|
|
||||||
result1 = store.search(("user_123", "memories"), query="这是中文")
|
|
||||||
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
|
||||||
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
|
||||||
result4 = store.search(("user_123", "memories"), query="Это русский")
|
|
||||||
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
|
||||||
|
|
||||||
assert result1[0].key == "1"
|
|
||||||
assert result2[0].key == "2"
|
|
||||||
assert result3[0].key == "3"
|
|
||||||
assert result4[0].key == "4"
|
|
||||||
assert result5[0].key == "5"
|
|
||||||
|
|||||||
Generated
+822
-781
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
from collections.abc import Sequence
|
|
||||||
from typing import Annotated, Literal, TypedDict
|
|
||||||
|
|
||||||
from langchain_core.messages import BaseMessage
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langgraph.graph import END, StateGraph, add_messages
|
|
||||||
from langgraph.prebuilt import ToolNode
|
|
||||||
|
|
||||||
tools = []
|
|
||||||
|
|
||||||
model_oai = ChatOpenAI(temperature=0)
|
|
||||||
|
|
||||||
model_oai = model_oai.bind_tools(tools)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentState(TypedDict):
|
|
||||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
|
||||||
|
|
||||||
|
|
||||||
# Define the function that determines whether to continue or not
|
|
||||||
def should_continue(state):
|
|
||||||
messages = state["messages"]
|
|
||||||
last_message = messages[-1]
|
|
||||||
# If there are no tool calls, then we finish
|
|
||||||
if not last_message.tool_calls:
|
|
||||||
return "end"
|
|
||||||
# Otherwise if there is, we continue
|
|
||||||
else:
|
|
||||||
return "continue"
|
|
||||||
|
|
||||||
|
|
||||||
# Define the function that calls the model
|
|
||||||
def call_model(state, config):
|
|
||||||
model = model_oai
|
|
||||||
messages = state["messages"]
|
|
||||||
response = model.invoke(messages)
|
|
||||||
# We return a list, because this will get added to the existing list
|
|
||||||
return {"messages": [response]}
|
|
||||||
|
|
||||||
|
|
||||||
# Define the function to execute tools
|
|
||||||
tool_node = ToolNode(tools)
|
|
||||||
|
|
||||||
|
|
||||||
class ContextSchema(TypedDict):
|
|
||||||
model: Literal["anthropic", "openai"]
|
|
||||||
|
|
||||||
|
|
||||||
# Define a new graph
|
|
||||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
|
||||||
|
|
||||||
# Define the two nodes we will cycle between
|
|
||||||
workflow.add_node("agent", call_model)
|
|
||||||
workflow.add_node("action", tool_node)
|
|
||||||
|
|
||||||
# Set the entrypoint as `agent`
|
|
||||||
# This means that this node is the first one called
|
|
||||||
workflow.set_entry_point("agent")
|
|
||||||
|
|
||||||
# We now add a conditional edge
|
|
||||||
workflow.add_conditional_edges(
|
|
||||||
# First, we define the start node. We use `agent`.
|
|
||||||
# This means these are the edges taken after the `agent` node is called.
|
|
||||||
"agent",
|
|
||||||
# Next, we pass in the function that will determine which node is called next.
|
|
||||||
should_continue,
|
|
||||||
# Finally we pass in a mapping.
|
|
||||||
# The keys are strings, and the values are other nodes.
|
|
||||||
# END is a special node marking that the graph should finish.
|
|
||||||
# What will happen is we will call `should_continue`, and then the output of that
|
|
||||||
# will be matched against the keys in this mapping.
|
|
||||||
# Based on which one it matches, that node will then be called.
|
|
||||||
{
|
|
||||||
# If `tools`, then we call the tool node.
|
|
||||||
"continue": "action",
|
|
||||||
# Otherwise we finish.
|
|
||||||
"end": END,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# We now add a normal edge from `tools` to `agent`.
|
|
||||||
# This means that after `tools` is called, `agent` node is called next.
|
|
||||||
workflow.add_edge("action", "agent")
|
|
||||||
|
|
||||||
# Finally, we compile it!
|
|
||||||
# This compiles it into a LangChain Runnable,
|
|
||||||
# meaning you can use it as you would any other runnable
|
|
||||||
graph = workflow.compile()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "graph-prerelease-reqs-additional-deps"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Test for prerelease stuff"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = [
|
|
||||||
"langgraph==1.0.2"
|
|
||||||
]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "graph-prerelease-reqs-zuper-deps"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Test for prerelease stuff"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = [
|
|
||||||
"langchain-openai==1.0.1"
|
|
||||||
]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"python_version": "3.12",
|
|
||||||
"dependencies": [
|
|
||||||
".",
|
|
||||||
"./deps/additional_deps",
|
|
||||||
"./deps/zuper_deps"
|
|
||||||
],
|
|
||||||
"graphs": {
|
|
||||||
"agent": "./agent.py:graph"
|
|
||||||
},
|
|
||||||
"env": "../.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "graph-prerelease-reqs"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Test for prerelease stuff"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = [
|
|
||||||
"langchain-openai==1.0.0a2",
|
|
||||||
"langchain-anthropic==1.0.0a5",
|
|
||||||
"langgraph==1.0.2"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
prerelease = "allow"
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user