mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-11 18:55:17 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701907adf4 |
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -71,3 +71,4 @@ jobs:
|
||||
working-directory: libs/cli/js-examples
|
||||
run: |
|
||||
langgraph build -t langgraph-test-e
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
description: "From which folder this pipeline executes"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
# This env var allows us to get inline annotations when ruff has complaints.
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -50,6 +50,12 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry check
|
||||
|
||||
- name: Check lock file
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
# Also installs dev/lint/test/typing dependencies, to ensure we have
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
description: "From which folder this pipeline executes"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -39,6 +39,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
|
||||
|
||||
- name: Check Lock
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
description: "From which folder this pipeline executes"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
PYTHON_VERSION: "3.10"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- "libs/**"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- "libs/**"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
|
||||
@@ -17,7 +17,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
@@ -42,8 +42,8 @@ jobs:
|
||||
|
||||
- name: Check README.md is in sync
|
||||
run: |
|
||||
if ! diff -q README.md libs/langgraph/README.md >/dev/null; then
|
||||
echo "README.md is out of sync with libs/langgraph/README.md"
|
||||
diff -C 3 README.md libs/langgraph/README.md
|
||||
exit 1
|
||||
fi
|
||||
if ! diff -q README.md libs/langgraph/README.md >/dev/null; then
|
||||
echo "README.md is out of sync with libs/langgraph/README.md"
|
||||
diff -C 3 README.md libs/langgraph/README.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
POETRY_VERSION: "2.1.2"
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
type: string
|
||||
description: "JSON string of changed files"
|
||||
schedule:
|
||||
- cron: "0 13 * * *"
|
||||
- cron: '0 13 * * *'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: 3.11
|
||||
poetry-version: 2.1.2
|
||||
poetry-version: 1.7.1
|
||||
cache-key: test-langgraph-notebooks
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with test --no-root
|
||||
poetry install --with test
|
||||
poetry run pip install jupyter
|
||||
|
||||
- name: Start services
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# How to integrate LangGraph into your React application
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
!!! info "Prerequisites" - [LangGraph Platform](../../concepts/langgraph_platform.md) - [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
|
||||
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
LangGraph Platform provides a flexible authentication and authorization system that can integrate with most authentication schemes.
|
||||
|
||||
!!! note "Python only"
|
||||
|
||||
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Authentication vs Authorization
|
||||
@@ -142,7 +146,7 @@ The returned user information is available:
|
||||
|
||||
After authentication, LangGraph calls your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can:
|
||||
|
||||
1. Add metadata to be saved during resource creation by mutating the `value["metadata"]` dictionary directly. See the [supported actions table](#supported-actions) for the list of types the value can take for each action.
|
||||
1. Add metadata to be saved during resource creation by mutating the `value["metadata"]` dictionary directly. See the [supported actions table](##supported-actions) for the list of types the value can take for each action.
|
||||
2. Filter resources by metadata during search/list or read operations by returning a [filter dictionary](#filter-operations).
|
||||
3. Raise an HTTP exception if access is denied.
|
||||
|
||||
@@ -285,7 +289,7 @@ async def on_assistant_create(
|
||||
)
|
||||
```
|
||||
|
||||
Notice that we are mixing global and resource-specific handlers in the above example. Since each request is handled by the most specific handler, a request to create a `thread` would match the `on_thread_create` handler but NOT the `reject_unhandled_requests` handler. A request to `update` a thread, however would be handled by the global handler, since we don't have a more specific handler for that resource and action.
|
||||
Notice that we are mixing global and resource-specific handlers in the above example. Since each request is handled by the most specific handler, a request to create a `thread` would match the `on_thread_create` handler but NOT the `reject_unhandled_requests` handler. A request to `update` a thread, however would be handled by the global handler, since we don't have a more specific handler for that resource and action. Requests to create, update,
|
||||
|
||||
### Filter Operations {#filter-operations}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
This guide assumes familiarity with the [LangGraph Platform](../../concepts/index.md#langgraph-platform), [Persistence](../../concepts/persistence.md), and [Cross-thread persistence](../../concepts/persistence.md#memory-store) concepts.
|
||||
This guide assumes familiarity with the [LangGraph Platform](../../concepts/index.md#langgraph-platform), [Persistence](../../concepts/persistence.md), and [Cross-thread persistence](../../concepts/store.md) concepts.
|
||||
|
||||
???+ note "LangGraph platform only"
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import Any, Optional
|
||||
@@ -151,7 +150,7 @@ def _dump_blobs(
|
||||
checkpoint_ns: str,
|
||||
values: dict[str, Any],
|
||||
versions: ChannelVersions,
|
||||
) -> list[tuple[str, str, str, str, Optional[bytes]]]:
|
||||
) -> list[tuple[str, str, str, str, str, Optional[bytes]]]:
|
||||
if not versions:
|
||||
return []
|
||||
|
||||
@@ -189,12 +188,6 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
pipe: Optional[Pipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
"Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., checkpoint_during=False)`.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(serde=serde)
|
||||
if isinstance(conn, ConnectionPool) and pipe is not None:
|
||||
raise ValueError(
|
||||
@@ -535,12 +528,6 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
pipe: Optional[AsyncPipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
"Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., checkpoint_during=False)`.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(serde=serde)
|
||||
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
||||
raise ValueError(
|
||||
|
||||
@@ -406,18 +406,6 @@ class Config(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
PIP_CLEANUP_LINES = """# -- Ensure user deps didn't inadvertently overwrite langgraph-api
|
||||
RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
|
||||
touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir --no-deps -e /api
|
||||
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
|
||||
# -- Removing pip from the final image ~<:===~~~ --
|
||||
RUN pip uninstall -y pip setuptools wheel && \
|
||||
rm -rf /usr/local/lib/python*/site-packages/pip* /usr/local/lib/python*/site-packages/setuptools* /usr/local/lib/python*/site-packages/wheel* && \
|
||||
find /usr/local/bin -name "pip*" -delete
|
||||
# -- End of pip removal --"""
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
"""Parse a version string into a tuple of (major, minor)."""
|
||||
try:
|
||||
@@ -1153,8 +1141,6 @@ ADD {relpath} /deps/{name}
|
||||
"",
|
||||
ui_inst_str,
|
||||
"",
|
||||
PIP_CLEANUP_LINES, # Add pip cleanup after all installations are complete
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
|
||||
|
||||
Generated
+20
-33
@@ -20,7 +20,7 @@ description = "High level compatibility layer for multiple asynchronous event lo
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
|
||||
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
|
||||
@@ -59,7 +59,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
@@ -405,7 +405,7 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
@@ -418,7 +418,7 @@ description = "A minimal low-level HTTP client."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
|
||||
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
|
||||
@@ -441,7 +441,7 @@ description = "The next generation HTTP client."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
@@ -467,7 +467,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
@@ -614,27 +614,27 @@ langgraph-sdk = ">=0.1.42,<0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.1.0"
|
||||
version = "0.0.42"
|
||||
description = ""
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.11.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_api-0.1.0-py3-none-any.whl", hash = "sha256:93eb369849d5ea0dd6076f0a36fe0a9669415c46e95fcb28ccf30c73e13e5e01"},
|
||||
{file = "langgraph_api-0.1.0.tar.gz", hash = "sha256:d02201d34172f3020af8f21c4560142b302474cee4a27f4335fbd950ab22f22f"},
|
||||
{file = "langgraph_api-0.0.42-py3-none-any.whl", hash = "sha256:19f69d9d39efde60a9bd3eeae6dc7dbe8d04b1b6fccf4ddf51d7e6b7187cc6ea"},
|
||||
{file = "langgraph_api-0.0.42.tar.gz", hash = "sha256:a0a18545c73f9703d5d5907fc030e4a0acb79d1e6b79d4e38b3cac2bfb470e97"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blockbuster = ">=1.5.24,<2.0.0"
|
||||
cloudpickle = ">=3.0.0,<4.0.0"
|
||||
cryptography = ">=42.0.0,<45.0"
|
||||
cryptography = ">=43.0.3,<44.0.0"
|
||||
httpx = ">=0.25.0"
|
||||
jsonschema-rs = ">=0.20.0,<0.30"
|
||||
langchain-core = ">=0.2.38,<0.4.0"
|
||||
langgraph = ">=0.2.56,<0.4.0"
|
||||
langgraph-checkpoint = ">=2.0.23,<3.0"
|
||||
langgraph-sdk = ">=0.1.61,<0.2.0"
|
||||
langgraph-sdk = ">=0.1.59,<0.2.0"
|
||||
langsmith = ">=0.1.63,<0.4.0"
|
||||
orjson = ">=3.9.7"
|
||||
pyjwt = ">=2.9.0,<3.0.0"
|
||||
@@ -679,30 +679,17 @@ files = [
|
||||
langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.3.15 || >0.3.15,<0.3.16 || >0.3.16,<0.3.17 || >0.3.17,<0.3.18 || >0.3.18,<0.3.19 || >0.3.19,<0.3.20 || >0.3.20,<0.3.21 || >0.3.21,<0.3.22 || >0.3.22,<0.4.0"
|
||||
langgraph-checkpoint = ">=2.0.10,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-runtime-inmem"
|
||||
version = "0.0.1"
|
||||
description = "Inmem implementation for the LangGraph API server."
|
||||
optional = true
|
||||
python-versions = ">=3.11"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_runtime_inmem-0.0.1-py3-none-any.whl", hash = "sha256:a25ec8e3219f2fd60450de38412d24c83fbf0b2521c13871cc26ce9a68ead496"},
|
||||
{file = "langgraph_runtime_inmem-0.0.1.tar.gz", hash = "sha256:144bf5217efec4969f7f9c5e8279d6914cc5133d4b1a19e466b4966fa05f00c5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.61"
|
||||
version = "0.1.60"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_sdk-0.1.61-py3-none-any.whl", hash = "sha256:f2d774b12497c428862993090622d51e0dbc3f53e0cee3d74a13c7495d835cc6"},
|
||||
{file = "langgraph_sdk-0.1.61.tar.gz", hash = "sha256:87dd1f07ab82da8875ac343268ece8bf5414632017ebc9d1cef4b523962fd601"},
|
||||
{file = "langgraph_sdk-0.1.60-py3-none-any.whl", hash = "sha256:953df85b0a6cc3a106f0496ce8f950a65d88b3ba8198c3b4bb58a54469b256a9"},
|
||||
{file = "langgraph_sdk-0.1.60.tar.gz", hash = "sha256:7857a4a2a20a6a4c9934d1e7b5145eda92e3bc7286121813de2464d071050f88"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -864,7 +851,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
|
||||
@@ -1427,7 +1414,7 @@ description = "Sniff out which async library your code is running under"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -1563,7 +1550,7 @@ files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.11\" and (python_version < \"3.13\" or extra == \"inmem\")"}
|
||||
markers = {main = "python_version >= \"3.11\" and extra == \"inmem\""}
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
@@ -1847,9 +1834,9 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\
|
||||
cffi = ["cffi (>=1.11)"]
|
||||
|
||||
[extras]
|
||||
inmem = ["langgraph-api", "langgraph-runtime-inmem", "python-dotenv"]
|
||||
inmem = ["langgraph-api", "python-dotenv"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "afc2f8776b4b6144bd1197df49ba34089889e2a1110b8470d8f1b212e0b08380"
|
||||
content-hash = "4a45d739795019ae00e18ba8b0d366209deca9c5a5e65e9f387e5cf1d5aef187"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.2.1"
|
||||
version = "0.1.89"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,9 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.1.0,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-runtime-inmem = { version = ">=0.0.1,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-sdk = { version = ">=0.1.0,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.0.42,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -30,7 +28,7 @@ mypy = "^1.10.0"
|
||||
msgspec = "^0.19.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
inmem = ["langgraph-api", "langgraph-runtime-inmem", "python-dotenv"]
|
||||
inmem = ["langgraph-api", "python-dotenv"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
|
||||
@@ -2,14 +2,13 @@ import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from langgraph_cli.cli import cli, prepare_args_and_stdin
|
||||
from langgraph_cli.config import PIP_CLEANUP_LINES, Config, validate_config
|
||||
from langgraph_cli.config import Config, validate_config
|
||||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
|
||||
@@ -144,7 +143,6 @@ services:
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
WORKDIR /deps/cli
|
||||
|
||||
develop:
|
||||
|
||||
@@ -2,13 +2,11 @@ import json
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import textwrap
|
||||
|
||||
import click
|
||||
import pytest
|
||||
|
||||
from langgraph_cli.config import (
|
||||
PIP_CLEANUP_LINES,
|
||||
config_to_compose,
|
||||
config_to_docker,
|
||||
validate_config,
|
||||
@@ -210,7 +208,7 @@ def test_config_to_docker_simple():
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Installing local requirements --
|
||||
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
@@ -244,9 +242,8 @@ RUN set -ex && \\
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{PIP_CLEANUP_LINES}
|
||||
ENV LANGGRAPH_HTTP='{"app": "/deps/examples/my_app.py:app"}'
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
@@ -266,8 +263,7 @@ def test_config_to_docker_outside_path():
|
||||
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
@@ -295,12 +291,8 @@ RUN set -ex && \\
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
"""
|
||||
+ PIP_CLEANUP_LINES
|
||||
+ """
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
)
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {
|
||||
"__outer_tests": str(pathlib.Path(__file__).parent.parent.absolute()),
|
||||
@@ -320,8 +312,7 @@ def test_config_to_docker_pipconfig():
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
@@ -339,12 +330,8 @@ RUN set -ex && \\
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
"""
|
||||
+ PIP_CLEANUP_LINES
|
||||
+ """
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
)
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
@@ -381,7 +368,7 @@ def test_config_to_docker_local_deps():
|
||||
),
|
||||
"langchain/langgraph-api-custom",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
@@ -397,8 +384,7 @@ RUN set -ex && \\
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
||||
{PIP_CLEANUP_LINES}\
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
@@ -425,8 +411,7 @@ dependencies = ["langchain"]"""
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = (
|
||||
"""FROM langchain/langgraph-api:3.11
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/unit_tests
|
||||
# -- End of local package . --
|
||||
@@ -434,12 +419,7 @@ ADD . /deps/unit_tests
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||
"""
|
||||
+ PIP_CLEANUP_LINES
|
||||
+ "\n"
|
||||
+ "WORKDIR /deps/unit_tests"
|
||||
""
|
||||
)
|
||||
WORKDIR /deps/unit_tests"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
@@ -459,7 +439,7 @@ def test_config_to_docker_end_to_end():
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.12
|
||||
ARG meow
|
||||
ARG foo
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
@@ -478,8 +458,7 @@ RUN set -ex && \\
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
||||
{PIP_CLEANUP_LINES}"""
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
@@ -494,7 +473,6 @@ def test_config_to_docker_nodejs():
|
||||
"node_version": "20",
|
||||
"graphs": graphs,
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
"auth": {"path": "./graphs/auth.mts:auth"},
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
@@ -506,7 +484,6 @@ ARG meow
|
||||
ARG foo
|
||||
ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
|
||||
@@ -532,7 +509,7 @@ def test_config_to_docker_gen_ui_python():
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||
RUN /storage/install-node.sh
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
@@ -548,13 +525,12 @@ RUN set -ex && \\
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
# -- Installing UI dependencies --
|
||||
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
|
||||
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of UI dependencies install --
|
||||
{PIP_CLEANUP_LINES}
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
@@ -564,8 +540,8 @@ WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
||||
# config_to_compose
|
||||
def test_config_to_compose_simple_config():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
# Create a properly indented version of PIP_CLEANUP_LINES for compose files
|
||||
expected_compose_stdin = f"""
|
||||
expected_compose_stdin = """\
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
@@ -585,8 +561,7 @@ def test_config_to_compose_simple_config():
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
actual_compose_stdin = config_to_compose(
|
||||
@@ -594,15 +569,12 @@ def test_config_to_compose_simple_config():
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
assert (
|
||||
clean_empty_lines(actual_compose_stdin).strip()
|
||||
== expected_compose_stdin.strip()
|
||||
)
|
||||
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||
|
||||
|
||||
def test_config_to_compose_env_vars():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
expected_compose_stdin = f""" OPENAI_API_KEY: "key"
|
||||
expected_compose_stdin = """ OPENAI_API_KEY: "key"
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
@@ -623,8 +595,7 @@ def test_config_to_compose_env_vars():
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
openai_api_key = "key"
|
||||
@@ -644,7 +615,7 @@ def test_config_to_compose_env_vars():
|
||||
|
||||
def test_config_to_compose_env_file():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
expected_compose_stdin = f"""\
|
||||
expected_compose_stdin = """\
|
||||
env_file: .env
|
||||
pull_policy: build
|
||||
build:
|
||||
@@ -665,8 +636,7 @@ def test_config_to_compose_env_file():
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
actual_compose_stdin = config_to_compose(
|
||||
@@ -679,7 +649,7 @@ def test_config_to_compose_env_file():
|
||||
|
||||
def test_config_to_compose_watch():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
expected_compose_stdin = f"""\
|
||||
expected_compose_stdin = """\
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
@@ -700,8 +670,7 @@ def test_config_to_compose_watch():
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
develop:
|
||||
@@ -723,7 +692,7 @@ def test_config_to_compose_watch():
|
||||
def test_config_to_compose_end_to_end():
|
||||
# test all of the above + langgraph API path
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
expected_compose_stdin = f"""\
|
||||
expected_compose_stdin = """\
|
||||
env_file: .env
|
||||
pull_policy: build
|
||||
build:
|
||||
@@ -744,8 +713,7 @@ def test_config_to_compose_end_to_end():
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
develop:
|
||||
|
||||
@@ -26,7 +26,6 @@ async def arun(graph: Pregel, input: dict):
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -43,7 +42,6 @@ async def arun_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -63,7 +61,6 @@ def run(graph: Pregel, input: dict):
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -80,7 +77,6 @@ def run_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -83,8 +83,6 @@ CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous")
|
||||
# holds the previous return value from a stateful Pregel graph.
|
||||
CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
|
||||
# holds a function that receives tasks from runner, executes them and returns results
|
||||
CONFIG_KEY_CHECKPOINT_DURING = sys.intern("__pregel_checkpoint_during")
|
||||
# holds a boolean indicating whether to checkpoint during the run (or only at the end)
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
|
||||
@@ -1,320 +1,55 @@
|
||||
import functools
|
||||
import logging
|
||||
import weakref
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Annotated
|
||||
|
||||
__all__ = ["SchemaCoercionMapper"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], "SchemaCoercionMapper"] = (
|
||||
weakref.WeakKeyDictionary()
|
||||
)
|
||||
|
||||
|
||||
class SchemaCoercionMapper:
|
||||
"""Lightweight coercion of *dict* → *BaseModel* instances."""
|
||||
__slots__ = ("_inited", "schema", "_fields", "_construct", "_field_coercers")
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
schema: Type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
**kwargs: Any,
|
||||
) -> "SchemaCoercionMapper":
|
||||
by_depth = _cache.setdefault(schema, {})
|
||||
if max_depth in by_depth:
|
||||
return by_depth[max_depth]
|
||||
if schema in _cache:
|
||||
return _cache[schema]
|
||||
inst = super().__new__(cls)
|
||||
by_depth[max_depth] = inst
|
||||
_cache[schema] = inst
|
||||
return inst
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: Type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
) -> None:
|
||||
if hasattr(self, "_initialised"):
|
||||
**kwargs: Any,
|
||||
):
|
||||
if hasattr(self, "_inited"):
|
||||
return
|
||||
self._initialised = True
|
||||
|
||||
self.schema = schema
|
||||
self.max_depth = max_depth
|
||||
|
||||
self.type_hints = (
|
||||
type_hints
|
||||
if type_hints is not None
|
||||
else get_type_hints(schema, localns={schema.__name__: schema})
|
||||
)
|
||||
|
||||
self._inited = True
|
||||
if issubclass(schema, BaseModelV1):
|
||||
self._fields = {
|
||||
n: self.type_hints.get(n, f.annotation)
|
||||
for n, f in schema.__fields__.items()
|
||||
}
|
||||
self._construct = schema.construct
|
||||
self._construct: Callable[..., Any] = schema.parse_obj
|
||||
|
||||
elif issubclass(schema, BaseModel):
|
||||
self._fields = {
|
||||
n: self.type_hints.get(n, f.annotation)
|
||||
for n, f in schema.model_fields.items()
|
||||
}
|
||||
self._construct: Callable[..., Any] = schema.model_construct # type: ignore
|
||||
self._construct = schema.model_validate
|
||||
|
||||
else:
|
||||
raise TypeError("Schema is neither a Pydantic v1 nor v2 model.")
|
||||
|
||||
self._field_coercers: Optional[dict[str, Callable[[Any, int], Any]]] = None
|
||||
raise TypeError("Schema is neither valid Pydantic v1 nor v2 model.")
|
||||
|
||||
def __call__(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
return self.coerce(input_data, depth)
|
||||
|
||||
def coerce(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
if depth is None:
|
||||
depth = self.max_depth
|
||||
if not isinstance(input_data, dict) or depth <= 0:
|
||||
if not isinstance(input_data, dict):
|
||||
return input_data
|
||||
|
||||
if self._field_coercers is None:
|
||||
self._field_coercers = {
|
||||
n: self._build_coercer(t, depth - 1) for n, t in self._fields.items()
|
||||
}
|
||||
|
||||
processed: dict[str, Any] = {}
|
||||
for k, v in input_data.items():
|
||||
fn = self._field_coercers.get(k)
|
||||
processed[k] = fn(v, depth - 1) if fn else v
|
||||
return self._construct(**processed)
|
||||
|
||||
def _build_coercer(
|
||||
self, field_type: Any, depth: int, *, throw: bool = False
|
||||
) -> Callable[[Any, Any], Any]:
|
||||
if depth == 0:
|
||||
return self._passthrough
|
||||
|
||||
origin = get_origin(field_type)
|
||||
|
||||
if (field_type in _IDENTITY_TYPES) or (origin in _IDENTITY_TYPES):
|
||||
return self._passthrough
|
||||
|
||||
if origin is Annotated:
|
||||
real_type, *_ = get_args(field_type)
|
||||
sub = self._build_coercer(real_type, depth - 1)
|
||||
return lambda v, d: sub(v, d)
|
||||
|
||||
if isclass(field_type):
|
||||
# This is needed bcs. of issubclass issues on older versions of python
|
||||
is_class_ = True
|
||||
try:
|
||||
is_bm_v2 = issubclass(field_type, BaseModel)
|
||||
except TypeError:
|
||||
# python < 3.11 issue.
|
||||
is_class_ = False
|
||||
is_bm_v2 = False
|
||||
if is_bm_v2 or (is_class_ and issubclass(field_type, BaseModelV1)):
|
||||
mapper = SchemaCoercionMapper(field_type, max_depth=depth - 1)
|
||||
return lambda v, d: mapper.coerce(v, d) if isinstance(v, dict) else v
|
||||
|
||||
if origin is list:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 1:
|
||||
return self._passthrough
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
|
||||
def list_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple)):
|
||||
return v
|
||||
return [sub(x, d - 1) for x in v]
|
||||
|
||||
return list_coercer
|
||||
|
||||
if origin is set or field_type is set:
|
||||
args = get_args(field_type)
|
||||
if len(args) > 1:
|
||||
return self._passthrough
|
||||
elif len(args) == 1:
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
else:
|
||||
sub = None # type: ignore
|
||||
|
||||
def set_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple, set)):
|
||||
return v
|
||||
if sub is None:
|
||||
return set(v)
|
||||
return {sub(x, d - 1) for x in v}
|
||||
|
||||
return set_coercer
|
||||
if origin is dict or field_type is dict:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 2:
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
return v
|
||||
|
||||
return dict_coercer
|
||||
k_sub = self._build_coercer(args[0], depth - 1)
|
||||
v_sub = self._build_coercer(args[1], depth - 1)
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
return v
|
||||
return {k_sub(k, d - 1): v_sub(val, d - 1) for k, val in v.items()}
|
||||
|
||||
return dict_coercer
|
||||
|
||||
if origin is tuple:
|
||||
elem_types = get_args(field_type)
|
||||
if not elem_types:
|
||||
return self._passthrough
|
||||
subs = [self._build_coercer(t, depth - 1) for t in elem_types]
|
||||
return lambda v, d: (
|
||||
tuple(
|
||||
subs[i](v[i] if i < len(v) else None, d - 1)
|
||||
for i in range(len(subs))
|
||||
)
|
||||
if isinstance(v, (list, tuple))
|
||||
else v
|
||||
)
|
||||
|
||||
if origin is Union:
|
||||
uargs = get_args(field_type)
|
||||
subs, none_in_union = [], False
|
||||
for ix, arg in enumerate(uargs):
|
||||
if arg is type(None):
|
||||
none_in_union = True
|
||||
else:
|
||||
subs.append(
|
||||
self._build_coercer(arg, depth - 1, throw=ix < len(uargs) - 1)
|
||||
)
|
||||
|
||||
def union_coercer(v: Any, d: Any) -> Any:
|
||||
if v is None and none_in_union:
|
||||
return None
|
||||
err = None
|
||||
for sp in subs:
|
||||
try:
|
||||
return sp(v, d - 1)
|
||||
except TypeError as e:
|
||||
err = e
|
||||
if err:
|
||||
raise err
|
||||
return v
|
||||
|
||||
return union_coercer
|
||||
|
||||
adapter_fn = _get_adapter(field_type)
|
||||
return lambda v, _d: adapter_fn(v)
|
||||
|
||||
@staticmethod
|
||||
def _passthrough(v: Any, _d: Any) -> Any: # noqa: D401
|
||||
return v
|
||||
|
||||
|
||||
_adapter_cache: dict[Any, Callable[[Any], Any]] = {}
|
||||
|
||||
|
||||
_IDENTITY_TYPES: tuple[type[Any], ...] = (
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
bool,
|
||||
bytes,
|
||||
bytearray,
|
||||
complex,
|
||||
memoryview,
|
||||
type(None),
|
||||
)
|
||||
|
||||
try:
|
||||
# Pydantic v2.
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
try:
|
||||
import pydantic.v1.types as v1_types_
|
||||
from pydantic.v1 import parse_obj_as
|
||||
|
||||
v1_types = tuple(
|
||||
v for k, v in vars(v1_types_).items() if k in v1_types_.__all__
|
||||
)
|
||||
except ImportError:
|
||||
v1_types = ()
|
||||
|
||||
def parse_obj_as(tp: Any, v: Any) -> Any: # type: ignore
|
||||
return v
|
||||
|
||||
try:
|
||||
from pydantic.v1 import parse_obj_as
|
||||
from pydantic.v1.main import create_model
|
||||
except ImportError:
|
||||
create_model = None # type: ignore
|
||||
|
||||
def _get_v1_parser(tp: Any) -> Any:
|
||||
if create_model is not None:
|
||||
try:
|
||||
parser = create_model(
|
||||
f"ParsingModel[{tp}]",
|
||||
__root__=(tp, ...),
|
||||
)
|
||||
return lambda v: parser(__root__=v).__root__ # type: ignore
|
||||
except RuntimeError:
|
||||
return lambda v: v
|
||||
return lambda v: parse_obj_as(tp, v)
|
||||
|
||||
@functools.lru_cache(maxsize=2048)
|
||||
def _adapter_for(tp: Any) -> Callable[[Any], Any]: # noqa: D401
|
||||
if tp in v1_types:
|
||||
return _get_v1_parser(tp)
|
||||
try:
|
||||
return TypeAdapter(
|
||||
tp, config={"arbitrary_types_allowed": True}
|
||||
).validate_python
|
||||
except TypeError:
|
||||
# Delayed classes like ConstrainedList
|
||||
return _get_v1_parser(tp)
|
||||
|
||||
except ImportError:
|
||||
# Pydantic V1
|
||||
from pydantic.v1.main import create_model
|
||||
|
||||
@functools.lru_cache(maxsize=2048)
|
||||
def _adapter_for(tp: Any) -> Callable[[Any], Any]: # noqa: D401
|
||||
try:
|
||||
parser = create_model(
|
||||
f"ParsingModel[{tp}]",
|
||||
__root__=(tp, ...),
|
||||
)
|
||||
return lambda v: parser(__root__=v).__root__ # type: ignore
|
||||
except RuntimeError:
|
||||
return lambda v: v
|
||||
|
||||
|
||||
def _get_adapter(tp: Any) -> Callable[[Any], Any]:
|
||||
try:
|
||||
return _adapter_cache[tp]
|
||||
except KeyError:
|
||||
fn = _adapter_for(tp)
|
||||
_adapter_cache[tp] = fn
|
||||
return fn
|
||||
return self._construct(input_data)
|
||||
|
||||
@@ -776,13 +776,13 @@ class CompiledStateGraph(CompiledGraph):
|
||||
return updates
|
||||
elif (t := type(input)) and get_type_hints(t):
|
||||
# Pydantic v2
|
||||
if isinstance(input, BaseModelV1):
|
||||
keep: Optional[set[str]] = input.__fields_set__
|
||||
defaults = {k: v.default for k, v in t.__fields__.items()}
|
||||
elif isinstance(input, BaseModel):
|
||||
keep = input.model_fields_set
|
||||
if isinstance(input, BaseModel):
|
||||
keep: Optional[set[str]] = input.model_fields_set
|
||||
defaults = {k: v.default for k, v in input.model_fields.items()}
|
||||
# Pydantic v1
|
||||
elif isinstance(input, BaseModelV1):
|
||||
keep = input.__fields_set__
|
||||
defaults = {k: v.default for k, v in t.__fields__.items()}
|
||||
else:
|
||||
keep = None
|
||||
defaults = {}
|
||||
@@ -1060,7 +1060,7 @@ def _pick_mapper(
|
||||
if issubclass(schema, dict):
|
||||
return None
|
||||
if issubclass(schema, (BaseModel, BaseModelV1)):
|
||||
return SchemaCoercionMapper(schema, type_hints=type_hints)
|
||||
return SchemaCoercionMapper(schema)
|
||||
return partial(_coerce_state, schema)
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ from langgraph.checkpoint.base import (
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_DURING,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
@@ -2099,7 +2098,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
subgraphs: bool = False,
|
||||
) -> Iterator[Union[dict[str, Any], Any]]:
|
||||
@@ -2121,7 +2119,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: The keys to stream, defaults to all non-context channels.
|
||||
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
|
||||
interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
|
||||
checkpoint_during: Whether to checkpoint intermediate steps, defaults to True. If False, only the final checkpoint is saved.
|
||||
debug: Whether to print debug information during execution, defaults to False.
|
||||
subgraphs: Whether to stream subgraphs, defaults to False.
|
||||
|
||||
@@ -2283,9 +2280,6 @@ class Pregel(PregelProtocol):
|
||||
config[CONF][CONFIG_KEY_STREAM_WRITER] = lambda c: stream.put(
|
||||
((), "custom", c)
|
||||
)
|
||||
# set checkpointing mode for subgraphs
|
||||
if checkpoint_during is not None:
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during
|
||||
with SyncPregelLoop(
|
||||
input,
|
||||
input_model=self.input_model,
|
||||
@@ -2301,9 +2295,6 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
debug=debug,
|
||||
checkpoint_during=checkpoint_during
|
||||
if checkpoint_during is not None
|
||||
else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True),
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
migrate_checkpoint=self._migrate_checkpoint,
|
||||
) as loop:
|
||||
@@ -2386,7 +2377,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
subgraphs: bool = False,
|
||||
) -> AsyncIterator[Union[dict[str, Any], Any]]:
|
||||
@@ -2408,7 +2398,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: The keys to stream, defaults to all non-context channels.
|
||||
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
|
||||
interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
|
||||
checkpoint_during: Whether to checkpoint intermediate steps, defaults to True. If False, only the final checkpoint is saved.
|
||||
debug: Whether to print debug information during execution, defaults to False.
|
||||
subgraphs: Whether to stream subgraphs, defaults to False.
|
||||
|
||||
@@ -2590,9 +2579,6 @@ class Pregel(PregelProtocol):
|
||||
stream.put_nowait, ((), "custom", c)
|
||||
)
|
||||
)
|
||||
# set checkpointing mode for subgraphs
|
||||
if checkpoint_during is not None:
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during
|
||||
async with AsyncPregelLoop(
|
||||
input,
|
||||
input_model=self.input_model,
|
||||
@@ -2608,9 +2594,6 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
debug=debug,
|
||||
checkpoint_during=checkpoint_during
|
||||
if checkpoint_during is not None
|
||||
else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True),
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
migrate_checkpoint=self._migrate_checkpoint,
|
||||
) as loop:
|
||||
@@ -2686,7 +2669,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
@@ -2718,7 +2700,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys=output_keys,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
checkpoint_during=checkpoint_during,
|
||||
debug=debug,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -2740,7 +2721,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
@@ -2773,7 +2753,6 @@ class Pregel(PregelProtocol):
|
||||
output_keys=output_keys,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
checkpoint_during=checkpoint_during,
|
||||
debug=debug,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
@@ -63,7 +63,6 @@ from langgraph.constants import (
|
||||
RESUME,
|
||||
SCHEDULED,
|
||||
TAG_HIDDEN,
|
||||
TASKS,
|
||||
)
|
||||
from langgraph.errors import (
|
||||
CheckpointNotLatest,
|
||||
@@ -156,7 +155,7 @@ class PregelLoop(LoopProtocol):
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager]
|
||||
interrupt_after: Union[All, Sequence[str]]
|
||||
interrupt_before: Union[All, Sequence[str]]
|
||||
checkpoint_during: bool
|
||||
checkpoint_every_step: bool
|
||||
debug: bool
|
||||
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
@@ -181,7 +180,6 @@ class PregelLoop(LoopProtocol):
|
||||
channels: Mapping[str, BaseChannel]
|
||||
managed: ManagedValueMapping
|
||||
checkpoint: Checkpoint
|
||||
checkpoint_id_saved: str
|
||||
checkpoint_ns: tuple[str, ...]
|
||||
checkpoint_config: RunnableConfig
|
||||
checkpoint_metadata: CheckpointMetadata
|
||||
@@ -217,7 +215,7 @@ class PregelLoop(LoopProtocol):
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
checkpoint_during: bool = True,
|
||||
checkpoint_every_step: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
step=0,
|
||||
@@ -243,7 +241,7 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
self._migrate_checkpoint = migrate_checkpoint
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.checkpoint_during = checkpoint_during
|
||||
self.checkpoint_every_step = checkpoint_every_step
|
||||
self.debug = debug
|
||||
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
||||
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
|
||||
@@ -296,19 +294,29 @@ class PregelLoop(LoopProtocol):
|
||||
"""Put writes for a task, to be read by the next tick."""
|
||||
if not writes:
|
||||
return
|
||||
# always checkpoint writes containing Send, as they are fetched from the
|
||||
# parent checkpoint, not the current one
|
||||
checkpoint_during = self.checkpoint_during or any(w[0] == TASKS for w in writes)
|
||||
# deduplicate writes to special channels, last write wins
|
||||
if all(w[0] in WRITES_IDX_MAP for w in writes):
|
||||
writes = list({w[0]: w for w in writes}.values())
|
||||
# remove existing writes for this task
|
||||
self.checkpoint_pending_writes = [
|
||||
w for w in self.checkpoint_pending_writes if w[0] != task_id
|
||||
]
|
||||
# save writes
|
||||
self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
|
||||
if checkpoint_during and self.checkpointer_put_writes is not None:
|
||||
for c, v in writes:
|
||||
if (
|
||||
c in WRITES_IDX_MAP
|
||||
and (
|
||||
idx := next(
|
||||
(
|
||||
i
|
||||
for i, w in enumerate(self.checkpoint_pending_writes)
|
||||
if w[0] == task_id and w[1] == c
|
||||
),
|
||||
None,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
):
|
||||
self.checkpoint_pending_writes[idx] = (task_id, c, v)
|
||||
else:
|
||||
self.checkpoint_pending_writes.append((task_id, c, v))
|
||||
if self.checkpointer_put_writes is not None:
|
||||
config = patch_configurable(
|
||||
self.checkpoint_config,
|
||||
{
|
||||
@@ -341,46 +349,6 @@ class PregelLoop(LoopProtocol):
|
||||
if hasattr(self, "tasks"):
|
||||
self._output_writes(task_id, writes)
|
||||
|
||||
def _put_pending_writes(self) -> None:
|
||||
if self.checkpointer_put_writes is None:
|
||||
return
|
||||
if not self.checkpoint_pending_writes:
|
||||
return
|
||||
# patch config
|
||||
config = patch_configurable(
|
||||
self.checkpoint_config,
|
||||
{
|
||||
CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINT_NS, ""
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
|
||||
},
|
||||
)
|
||||
# group by task id
|
||||
by_task = defaultdict(list)
|
||||
for task_id, channel, value in self.checkpoint_pending_writes:
|
||||
by_task[task_id].append((channel, value))
|
||||
# submit writes to checkpointer
|
||||
for task_id, writes in by_task.items():
|
||||
if self.checkpointer_put_writes_accepts_task_path and hasattr(
|
||||
self, "tasks"
|
||||
):
|
||||
task = self.tasks.get(task_id)
|
||||
self.submit(
|
||||
self.checkpointer_put_writes,
|
||||
config,
|
||||
writes,
|
||||
task_id,
|
||||
task_path_str(task.path) if task else "",
|
||||
)
|
||||
else:
|
||||
self.submit(
|
||||
self.checkpointer_put_writes,
|
||||
config,
|
||||
writes,
|
||||
task_id,
|
||||
)
|
||||
|
||||
def accept_push(
|
||||
self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None
|
||||
) -> Optional[PregelExecutableTask]:
|
||||
@@ -743,44 +711,32 @@ class PregelLoop(LoopProtocol):
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
# assign step and parents
|
||||
exiting = metadata is self.checkpoint_metadata
|
||||
if exiting and self.checkpoint["id"] == self.checkpoint_id_saved:
|
||||
# checkpoint already saved
|
||||
return
|
||||
if not exiting:
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
self.checkpoint_metadata = metadata
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_checkpoint(
|
||||
metadata,
|
||||
self.channels,
|
||||
(
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys
|
||||
),
|
||||
)
|
||||
self.checkpoint_id_prev = self.checkpoint["id"] if self.step > -1 else None
|
||||
# do checkpoint?
|
||||
do_checkpoint = self._checkpointer_put_after_previous is not None and (
|
||||
exiting or self.checkpoint_during
|
||||
)
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(
|
||||
self.checkpoint,
|
||||
self.channels if do_checkpoint else None,
|
||||
self.step,
|
||||
id=self.checkpoint["id"] if exiting else None,
|
||||
)
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_checkpoint(
|
||||
metadata,
|
||||
self.channels,
|
||||
(
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys
|
||||
),
|
||||
)
|
||||
# bail if no checkpointer
|
||||
if do_checkpoint and self._checkpointer_put_after_previous is not None:
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
for k, v in self.config["metadata"].items():
|
||||
if k in EXCLUDED_METADATA_KEYS:
|
||||
continue
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(
|
||||
self.checkpoint, self.channels, self.step
|
||||
)
|
||||
self.checkpoint_metadata = metadata
|
||||
|
||||
self.prev_checkpoint_config = (
|
||||
self.checkpoint_config
|
||||
if CONFIG_KEY_CHECKPOINT_ID in self.checkpoint_config[CONF]
|
||||
@@ -791,8 +747,6 @@ class PregelLoop(LoopProtocol):
|
||||
**self.checkpoint_config,
|
||||
CONF: {
|
||||
**self.checkpoint_config[CONF],
|
||||
# this is guaranteed to be set by code above
|
||||
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint_id_prev,
|
||||
CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINT_NS, ""
|
||||
),
|
||||
@@ -823,9 +777,8 @@ class PregelLoop(LoopProtocol):
|
||||
CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
|
||||
},
|
||||
}
|
||||
if not exiting:
|
||||
# increment step
|
||||
self.step += 1
|
||||
# increment step
|
||||
self.step += 1
|
||||
|
||||
def _update_mv(self, key: str, values: Sequence[Any]) -> None:
|
||||
raise NotImplementedError
|
||||
@@ -836,10 +789,6 @@ class PregelLoop(LoopProtocol):
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
# persist current checkpoint and writes
|
||||
if not self.checkpoint_during:
|
||||
self._put_checkpoint(self.checkpoint_metadata)
|
||||
self._put_pending_writes()
|
||||
# suppress interrupt
|
||||
suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested
|
||||
if suppress:
|
||||
@@ -958,7 +907,6 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
checkpoint_during: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
input,
|
||||
@@ -977,7 +925,6 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
debug=debug,
|
||||
migrate_checkpoint=migrate_checkpoint,
|
||||
trigger_to_nodes=trigger_to_nodes,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
self.stack = ExitStack()
|
||||
if checkpointer:
|
||||
@@ -1057,7 +1004,6 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
},
|
||||
}
|
||||
self.prev_checkpoint_config = saved.parent_config
|
||||
self.checkpoint_id_saved = saved.checkpoint["id"]
|
||||
self.checkpoint = saved.checkpoint
|
||||
self.checkpoint_metadata = saved.metadata
|
||||
self.checkpoint_pending_writes = (
|
||||
@@ -1108,7 +1054,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
debug: bool = False,
|
||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
checkpoint_during: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
input,
|
||||
@@ -1127,7 +1072,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
debug=debug,
|
||||
migrate_checkpoint=migrate_checkpoint,
|
||||
trigger_to_nodes=trigger_to_nodes,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
self.stack = AsyncExitStack()
|
||||
if checkpointer:
|
||||
@@ -1207,7 +1151,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
},
|
||||
}
|
||||
self.prev_checkpoint_config = saved.parent_config
|
||||
self.checkpoint_id_saved = saved.checkpoint["id"]
|
||||
self.checkpoint = saved.checkpoint
|
||||
self.checkpoint_metadata = saved.metadata
|
||||
self.checkpoint_pending_writes = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from tests.conftest import (
|
||||
REGULAR_CHECKPOINTERS_ASYNC,
|
||||
REGULAR_CHECKPOINTERS_SYNC,
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interruption_without_state_updates(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
) -> None:
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
@@ -40,27 +40,20 @@ def test_interruption_without_state_updates(
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
graph.invoke(initial_input, thread, checkpoint_during=checkpoint_during)
|
||||
graph.invoke(initial_input, thread, debug=True)
|
||||
assert graph.get_state(thread).next == ("step_2",)
|
||||
n_checkpoints = len([c for c in graph.get_state_history(thread)])
|
||||
assert n_checkpoints == (3 if checkpoint_during else 1)
|
||||
|
||||
graph.invoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
graph.invoke(None, thread, debug=True)
|
||||
assert graph.get_state(thread).next == ("step_3",)
|
||||
n_checkpoints = len([c for c in graph.get_state_history(thread)])
|
||||
assert n_checkpoints == (4 if checkpoint_during else 2)
|
||||
|
||||
graph.invoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
graph.invoke(None, thread, debug=True)
|
||||
assert graph.get_state(thread).next == ()
|
||||
n_checkpoints = len([c for c in graph.get_state_history(thread)])
|
||||
assert n_checkpoints == (5 if checkpoint_during else 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_interruption_without_state_updates_async(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
checkpointer_name: str, mocker: MockerFixture
|
||||
):
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
|
||||
@@ -85,17 +78,11 @@ async def test_interruption_without_state_updates_async(
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during)
|
||||
await graph.ainvoke(initial_input, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_2",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (3 if checkpoint_during else 1)
|
||||
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_3",)
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (4 if checkpoint_during else 2)
|
||||
|
||||
await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during)
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ()
|
||||
n_checkpoints = len([c async for c in graph.aget_state_history(thread)])
|
||||
assert n_checkpoints == (5 if checkpoint_during else 3)
|
||||
|
||||
@@ -7258,10 +7258,9 @@ def test_branch_then(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_send_dedupe_on_resume(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@@ -7317,7 +7316,7 @@ def test_send_dedupe_on_resume(
|
||||
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert graph.invoke(["0"], thread1, checkpoint_during=checkpoint_during) == [
|
||||
assert graph.invoke(["0"], thread1, debug=1) == [
|
||||
"0",
|
||||
"1",
|
||||
"3.1",
|
||||
@@ -7334,11 +7333,12 @@ def test_send_dedupe_on_resume(
|
||||
pytest.xfail("TODO: shallow checkpointer reports wrong next set")
|
||||
assert state.next == ("flaky",)
|
||||
# check history
|
||||
history = [c for c in graph.get_state_history(thread1)]
|
||||
assert len(history) == (4 if checkpoint_during else 1)
|
||||
if "shallow" not in checkpointer_name:
|
||||
history = [c for c in graph.get_state_history(thread1)]
|
||||
assert len(history) == 4
|
||||
|
||||
# resume execution
|
||||
assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == [
|
||||
assert graph.invoke(None, thread1, debug=1) == [
|
||||
"0",
|
||||
"1",
|
||||
"3.1",
|
||||
@@ -7358,7 +7358,6 @@ def test_send_dedupe_on_resume(
|
||||
assert state.next == ()
|
||||
# check history
|
||||
history = [c for c in graph.get_state_history(thread1)]
|
||||
assert len(history) == (6 if checkpoint_during else 2)
|
||||
expected_history = [
|
||||
StateSnapshot(
|
||||
values=[
|
||||
@@ -7495,9 +7494,13 @@ def test_send_dedupe_on_resume(
|
||||
name="flaky",
|
||||
path=("__pregel_push", 1),
|
||||
error=None,
|
||||
interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),),
|
||||
interrupts=(
|
||||
Interrupt(
|
||||
value="Bahh", resumable=False, ns=None, when="during"
|
||||
),
|
||||
),
|
||||
state=None,
|
||||
result=["flaky|4"] if checkpoint_during else None,
|
||||
result=["flaky|4"],
|
||||
),
|
||||
PregelTask(
|
||||
id=AnyStr(),
|
||||
@@ -7634,11 +7637,10 @@ def test_send_dedupe_on_resume(
|
||||
),
|
||||
),
|
||||
]
|
||||
if checkpoint_during:
|
||||
assert history == expected_history
|
||||
else:
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[2]
|
||||
if "shallow" in checkpointer_name:
|
||||
expected_history = expected_history[:1]
|
||||
|
||||
assert history == expected_history
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import enum
|
||||
import functools
|
||||
import gc
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
import pathlib
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -17,7 +12,6 @@ from collections import Counter, deque
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from random import randrange
|
||||
from typing import (
|
||||
Annotated,
|
||||
@@ -1121,14 +1115,10 @@ def test_invoke_checkpoint_two(
|
||||
assert checkpoint["channel_values"].get("total") == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_pending_writes_resume(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
@@ -1154,19 +1144,17 @@ def test_pending_writes_resume(
|
||||
self.calls = 0
|
||||
|
||||
one = AwhileMaker(0.1, {"value": 2})
|
||||
two = AwhileMaker(0.2, ConnectionError("I'm not good"))
|
||||
two = AwhileMaker(0.3, ConnectionError("I'm not good"))
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("one", one)
|
||||
builder.add_node(
|
||||
"two", two, retry=RetryPolicy(max_attempts=2, initial_interval=0, jitter=False)
|
||||
)
|
||||
builder.add_node("two", two, retry=RetryPolicy(max_attempts=2))
|
||||
builder.add_edge(START, "one")
|
||||
builder.add_edge(START, "two")
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
thread1: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
||||
with pytest.raises(ConnectionError, match="I'm not good"):
|
||||
graph.invoke({"value": 1}, thread1, checkpoint_during=checkpoint_during)
|
||||
graph.invoke({"value": 1}, thread1)
|
||||
|
||||
# both nodes should have been called once
|
||||
assert one.calls == 1
|
||||
@@ -1212,7 +1200,7 @@ def test_pending_writes_resume(
|
||||
|
||||
# resume execution
|
||||
with pytest.raises(ConnectionError, match="I'm not good"):
|
||||
graph.invoke(None, thread1, checkpoint_during=checkpoint_during)
|
||||
graph.invoke(None, thread1)
|
||||
|
||||
# node "one" succeeded previously, so shouldn't be called again
|
||||
assert one.calls == 1
|
||||
@@ -1226,9 +1214,7 @@ def test_pending_writes_resume(
|
||||
# resume execution, without exception
|
||||
two.rtn = {"value": 3}
|
||||
# both the pending write and the new write were applied, 1 + 2 + 3 = 6
|
||||
assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == {
|
||||
"value": 6
|
||||
}
|
||||
assert graph.invoke(None, thread1) == {"value": 6}
|
||||
|
||||
if "shallow" in checkpointer_name:
|
||||
assert len(list(checkpointer.list(thread1))) == 1
|
||||
@@ -1237,7 +1223,7 @@ def test_pending_writes_resume(
|
||||
# check all final checkpoints
|
||||
checkpoints = [c for c in checkpointer.list(thread1)]
|
||||
# we should have 3
|
||||
assert len(checkpoints) == (3 if checkpoint_during else 2)
|
||||
assert len(checkpoints) == 3
|
||||
# the last one not too interesting for this test
|
||||
assert checkpoints[0] == CheckpointTuple(
|
||||
config={
|
||||
@@ -1339,26 +1325,15 @@ def test_pending_writes_resume(
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"]
|
||||
if checkpoint_during
|
||||
else AnyStr(),
|
||||
"checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "value", 3),
|
||||
)
|
||||
if checkpoint_during
|
||||
else UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
# the write against the previous checkpoint is not saved, as it is
|
||||
# produced in a run where only the next checkpoint (the last) is saved
|
||||
),
|
||||
)
|
||||
if not checkpoint_during:
|
||||
return
|
||||
assert checkpoints[2] == CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
@@ -1516,14 +1491,8 @@ def test_send_sequences() -> None:
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_task(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
mapper_calls = 0
|
||||
|
||||
@@ -1589,7 +1558,7 @@ def test_imp_task(
|
||||
}
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [
|
||||
assert [*graph.stream([0, 1], thread1)] == [
|
||||
{"mapper": "00"},
|
||||
{"mapper": "11"},
|
||||
{
|
||||
@@ -1605,23 +1574,17 @@ def test_imp_task(
|
||||
]
|
||||
assert mapper_calls == 2
|
||||
|
||||
assert graph.invoke(
|
||||
Command(resume="answer"), thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert graph.invoke(Command(resume="answer"), thread1) == [
|
||||
"00answer",
|
||||
"11answer",
|
||||
]
|
||||
assert mapper_calls == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_nested(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
def mynode(input: list[str]) -> list[str]:
|
||||
@@ -1663,7 +1626,7 @@ def test_imp_nested(
|
||||
}
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [
|
||||
assert [*graph.stream([0, 1], thread1)] == [
|
||||
{"submapper": "0"},
|
||||
{"mapper": "00"},
|
||||
{"submapper": "1"},
|
||||
@@ -1680,22 +1643,16 @@ def test_imp_nested(
|
||||
},
|
||||
]
|
||||
|
||||
assert graph.invoke(
|
||||
Command(resume="answer"), thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert graph.invoke(Command(resume="answer"), thread1) == [
|
||||
"00answera",
|
||||
"11answera",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_stream_order(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@task()
|
||||
@@ -1718,10 +1675,7 @@ def test_imp_stream_order(
|
||||
return fut_baz.result()
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [
|
||||
c
|
||||
for c in graph.stream({"a": "0"}, thread1, checkpoint_during=checkpoint_during)
|
||||
] == [
|
||||
assert [c for c in graph.stream({"a": "0"}, thread1)] == [
|
||||
{
|
||||
"foo": (
|
||||
"0foo",
|
||||
@@ -2781,9 +2735,6 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2(
|
||||
checkpointer_name: str,
|
||||
) -> None:
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
IS_V1 = BaseModel is BaseModelV1
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
setup = mocker.Mock()
|
||||
@@ -2822,28 +2773,14 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2(
|
||||
class InnerObject(BaseModel):
|
||||
yo: int
|
||||
|
||||
if IS_V1:
|
||||
class State(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
class State(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
query: str
|
||||
inner: Annotated[InnerObject, lambda x, y: y]
|
||||
answer: Optional[str] = None
|
||||
docs: Annotated[list[str], sorted_add]
|
||||
client: Annotated[httpx.Client, Context(make_httpx_client)]
|
||||
|
||||
else:
|
||||
|
||||
class State(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
query: str
|
||||
inner: Annotated[InnerObject, lambda x, y: y]
|
||||
answer: Optional[str] = None
|
||||
docs: Annotated[list[str], sorted_add]
|
||||
client: Annotated[httpx.Client, Context(make_httpx_client)]
|
||||
query: str
|
||||
inner: Annotated[InnerObject, lambda x, y: y]
|
||||
answer: Optional[str] = None
|
||||
docs: Annotated[list[str], sorted_add]
|
||||
client: Annotated[httpx.Client, Context(make_httpx_client)]
|
||||
|
||||
class StateUpdate(BaseModel):
|
||||
query: Optional[str] = None
|
||||
@@ -3102,49 +3039,15 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
"""Test that nested Pydantic models are properly constructed from leaf nodes up."""
|
||||
|
||||
# Define nested Pydantic models
|
||||
# Import necessary modules
|
||||
|
||||
if version == "v1":
|
||||
from pydantic.v1 import ( # type: ignore
|
||||
BaseModel,
|
||||
ByteSize,
|
||||
Field,
|
||||
SecretStr,
|
||||
confloat,
|
||||
conint,
|
||||
conlist,
|
||||
constr,
|
||||
)
|
||||
from pydantic.v1 import BaseModel, Field
|
||||
else:
|
||||
from pydantic import ( # type: ignore
|
||||
BaseModel,
|
||||
ByteSize,
|
||||
Field,
|
||||
SecretStr,
|
||||
confloat,
|
||||
conint,
|
||||
conlist,
|
||||
constr,
|
||||
)
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
if BaseModel is BaseModelV1:
|
||||
pytest.skip("Cannot test pydantic v2 using installed version < 2")
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class NestedModel(BaseModel):
|
||||
value: int
|
||||
name: str
|
||||
|
||||
# For constrained types
|
||||
PositiveInt = Annotated[int, Field(gt=0)]
|
||||
NonNegativeFloat = Annotated[float, Field(ge=0)]
|
||||
|
||||
# Enum type
|
||||
class UserRole(Enum):
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
GUEST = "guest"
|
||||
|
||||
# Forward reference model
|
||||
class RecursiveModel(BaseModel):
|
||||
value: str
|
||||
@@ -3165,19 +3068,12 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
name: str
|
||||
friends: list[str] = Field(default_factory=list) # IDs of friends
|
||||
|
||||
if version == "v2":
|
||||
conlist_type = conlist(item_type=int, min_length=2, max_length=5)
|
||||
else:
|
||||
conlist_type = conlist(item_type=int, min_items=2, max_items=5)
|
||||
|
||||
class State(BaseModel):
|
||||
# Basic nested model tests
|
||||
top_level: str
|
||||
auuid: uuid.UUID
|
||||
nested: NestedModel
|
||||
optional_nested: Annotated[Optional[NestedModel], lambda x, y: y, "Foo"]
|
||||
dict_nested: dict[str, NestedModel]
|
||||
simple_str_list: list[str]
|
||||
list_nested: Annotated[
|
||||
Union[dict, list[dict[str, NestedModel]]], lambda x, y: (x or []) + [y]
|
||||
]
|
||||
@@ -3194,51 +3090,15 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
# Cyclic reference test
|
||||
people: dict[str, Person] # Map of ID -> Person
|
||||
|
||||
# Rich type adapters
|
||||
ip_address: ipaddress.IPv4Address
|
||||
ip_address_v6: ipaddress.IPv6Address
|
||||
amount: decimal.Decimal
|
||||
file_path: pathlib.Path
|
||||
timestamp: datetime.datetime
|
||||
date_only: datetime.date
|
||||
time_only: datetime.time
|
||||
duration: datetime.timedelta
|
||||
immutable_set: frozenset[int]
|
||||
binary_data: bytes
|
||||
pattern: re.Pattern
|
||||
secret: SecretStr
|
||||
file_size: ByteSize
|
||||
|
||||
# Constrained types
|
||||
positive_value: PositiveInt
|
||||
non_negative: NonNegativeFloat
|
||||
limited_string: constr(min_length=3, max_length=10)
|
||||
bounded_int: conint(ge=10, le=100)
|
||||
restricted_float: confloat(gt=0, lt=1)
|
||||
required_list: conlist_type
|
||||
|
||||
# Enum & Literal
|
||||
role: UserRole
|
||||
status: Literal["active", "inactive", "pending"]
|
||||
|
||||
# Annotated & NewType
|
||||
validated_age: Annotated[int, Field(gt=0, lt=120)]
|
||||
|
||||
# Generic containers with validators
|
||||
decimal_list: List[decimal.Decimal]
|
||||
id_tuple: tuple[uuid.UUID, uuid.UUID]
|
||||
|
||||
inputs = {
|
||||
# Basic nested models
|
||||
"top_level": "initial",
|
||||
"auuid": str(uuid.uuid4()),
|
||||
"nested": {"value": 42, "name": "test"},
|
||||
"optional_nested": {"value": 10, "name": "optional"},
|
||||
"dict_nested": {"a": {"value": 5, "name": "a"}},
|
||||
"list_nested": [{"a": {"value": 6, "name": "b"}}],
|
||||
"tuple_nested": ["tuple-key", {"value": 7, "name": "tuple-value"}],
|
||||
"tuple_list_nested": [[1, {"value": 8, "name": "tuple-in-list"}]],
|
||||
"simple_str_list": ["siss", "boom", "bah"],
|
||||
"complex_tuple": [
|
||||
"complex",
|
||||
{"nested": [9, {"value": 10, "name": "deep"}]},
|
||||
@@ -3265,35 +3125,6 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
"friends": ["1", "2"], # Charlie is friends with Alice and Bob
|
||||
},
|
||||
},
|
||||
# Rich type adapters
|
||||
"ip_address": "192.168.1.1",
|
||||
"ip_address_v6": "2001:db8::1",
|
||||
"amount": "123.45",
|
||||
"file_path": "/tmp/test.txt",
|
||||
"timestamp": "2025-04-07T10:58:04",
|
||||
"date_only": "2025-04-07",
|
||||
"time_only": "10:58:04",
|
||||
"duration": 3600, # seconds
|
||||
"immutable_set": [1, 2, 3, 4],
|
||||
"binary_data": b"hello world",
|
||||
"pattern": "^test$",
|
||||
"secret": "password123",
|
||||
"file_size": 1024,
|
||||
# Constrained types
|
||||
"positive_value": 42,
|
||||
"non_negative": 0.0,
|
||||
"limited_string": "test",
|
||||
"bounded_int": 50,
|
||||
"restricted_float": 0.5,
|
||||
"required_list": [10, 20, 30],
|
||||
# Enum & Literal
|
||||
"role": "admin",
|
||||
"status": "active",
|
||||
# Annotated & NewType
|
||||
"validated_age": 30,
|
||||
# Generic containers with validators
|
||||
"decimal_list": ["10.5", "20.75", "30.25"],
|
||||
"id_tuple": [str(uuid.uuid4()), str(uuid.uuid4())],
|
||||
}
|
||||
|
||||
update = {"top_level": "updated", "nested": {"value": 100, "name": "updated"}}
|
||||
@@ -3301,42 +3132,7 @@ def test_nested_pydantic_models(version: str) -> None:
|
||||
expected = State(**inputs)
|
||||
|
||||
def node_fn(state: State) -> dict:
|
||||
# Basic assertions
|
||||
assert isinstance(state.auuid, uuid.UUID)
|
||||
assert state == expected
|
||||
|
||||
# Rich type assertions
|
||||
assert isinstance(state.ip_address, ipaddress.IPv4Address)
|
||||
assert isinstance(state.ip_address_v6, ipaddress.IPv6Address)
|
||||
assert isinstance(state.amount, decimal.Decimal)
|
||||
assert isinstance(state.file_path, pathlib.Path)
|
||||
assert isinstance(state.timestamp, datetime.datetime)
|
||||
assert isinstance(state.date_only, datetime.date)
|
||||
assert isinstance(state.time_only, datetime.time)
|
||||
assert isinstance(state.duration, datetime.timedelta)
|
||||
assert isinstance(state.immutable_set, frozenset)
|
||||
assert isinstance(state.binary_data, bytes)
|
||||
assert isinstance(state.pattern, re.Pattern)
|
||||
|
||||
# Constrained types
|
||||
assert state.positive_value > 0
|
||||
assert state.non_negative >= 0
|
||||
assert 3 <= len(state.limited_string) <= 10
|
||||
assert 10 <= state.bounded_int <= 100
|
||||
assert 0 < state.restricted_float < 1
|
||||
assert 2 <= len(state.required_list) <= 5
|
||||
|
||||
# Enum & Literal
|
||||
assert state.role == UserRole.ADMIN
|
||||
assert state.status == "active"
|
||||
|
||||
# Annotated
|
||||
assert 0 < state.validated_age < 120
|
||||
|
||||
# Generic containers
|
||||
assert len(state.decimal_list) == 3
|
||||
assert len(state.id_tuple) == 2
|
||||
|
||||
return update
|
||||
|
||||
builder = StateGraph(State)
|
||||
@@ -3847,14 +3643,10 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None:
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_subgraph_checkpoint_true(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Unsupported combo")
|
||||
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
|
||||
class InnerState(TypedDict):
|
||||
@@ -3886,12 +3678,7 @@ def test_subgraph_checkpoint_true(
|
||||
app = graph.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
c
|
||||
for c in app.stream(
|
||||
{"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c for c in app.stream({"my_key": ""}, config, subgraphs=True)] == [
|
||||
(("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}),
|
||||
(("inner",), {"inner_2": {"my_key": " and there"}}),
|
||||
((), {"inner": {"my_key": " got here and there"}}),
|
||||
@@ -3916,14 +3703,10 @@ def test_subgraph_checkpoint_true(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_subgraph_checkpoint_true_interrupt(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Unsupported combo")
|
||||
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
|
||||
# Define subgraph
|
||||
@@ -3962,18 +3745,15 @@ def test_subgraph_checkpoint_true_interrupt(
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert graph.invoke(
|
||||
{"foo": "foo"}, config, checkpoint_during=checkpoint_during
|
||||
) == {"foo": "hi! foo"}
|
||||
assert graph.invoke({"foo": "foo"}, config) == {"foo": "hi! foo"}
|
||||
assert graph.get_state(config, subgraphs=True).tasks[0].state.values == {
|
||||
"bar": "hi! foo"
|
||||
}
|
||||
assert graph.invoke(
|
||||
Command(resume="baz"), config, checkpoint_during=checkpoint_during
|
||||
) == {"foo": "hi! foobaz"}
|
||||
assert graph.invoke(Command(resume="baz"), config) == {"foo": "hi! foobaz"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@@ -4089,14 +3869,10 @@ def test_stream_buffering_single_node(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_nested_graph_interrupts_parallel(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Unsupported combo")
|
||||
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
|
||||
class InnerState(TypedDict):
|
||||
@@ -4143,11 +3919,11 @@ def test_nested_graph_interrupts_parallel(
|
||||
|
||||
# test invoke w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert app.invoke({"my_key": ""}, config, checkpoint_during=checkpoint_during) == {
|
||||
assert app.invoke({"my_key": ""}, config, debug=True) == {
|
||||
"my_key": " and parallel",
|
||||
}
|
||||
|
||||
assert app.invoke(None, config, checkpoint_during=checkpoint_during) == {
|
||||
assert app.invoke(None, config, debug=True) == {
|
||||
"my_key": "got here and there and parallel and back again",
|
||||
}
|
||||
|
||||
@@ -4156,17 +3932,13 @@ def test_nested_graph_interrupts_parallel(
|
||||
# - the writes of outer are persisted in 1st call and used in 2nd call, ie outer isn't called again (because we dont see outer_1 output again in 2nd stream)
|
||||
# test stream updates w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
*app.stream(
|
||||
{"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream({"my_key": ""}, config, subgraphs=True)] == [
|
||||
# we got to parallel node first
|
||||
((), {"outer_1": {"my_key": " and parallel"}}),
|
||||
((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}),
|
||||
((), {"__interrupt__": ()}),
|
||||
]
|
||||
assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [
|
||||
assert [*app.stream(None, config)] == [
|
||||
{"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}},
|
||||
{"inner": {"my_key": "got here and there"}},
|
||||
{"outer_2": {"my_key": " and back again"}},
|
||||
@@ -4174,22 +3946,11 @@ def test_nested_graph_interrupts_parallel(
|
||||
|
||||
# test stream values w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "3"}}
|
||||
assert [
|
||||
*app.stream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
@@ -4198,28 +3959,15 @@ def test_nested_graph_interrupts_parallel(
|
||||
# test interrupts BEFORE the parallel node
|
||||
app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"])
|
||||
config = {"configurable": {"thread_id": "4"}}
|
||||
assert [
|
||||
*app.stream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
] == [{"my_key": ""}]
|
||||
assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [
|
||||
{"my_key": ""}
|
||||
]
|
||||
# while we're waiting for the node w/ interrupt inside to finish
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
@@ -4228,43 +3976,24 @@ def test_nested_graph_interrupts_parallel(
|
||||
# test interrupts AFTER the parallel node
|
||||
app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"])
|
||||
config = {"configurable": {"thread_id": "5"}}
|
||||
assert [
|
||||
*app.stream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
]
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_doubly_nested_graph_interrupts(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Unsupported combo")
|
||||
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
|
||||
class State(TypedDict):
|
||||
@@ -4318,13 +4047,11 @@ def test_doubly_nested_graph_interrupts(
|
||||
|
||||
# test invoke w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert app.invoke(
|
||||
{"my_key": "my value"}, config, checkpoint_during=checkpoint_during
|
||||
) == {
|
||||
assert app.invoke({"my_key": "my value"}, config, debug=True) == {
|
||||
"my_key": "hi my value",
|
||||
}
|
||||
|
||||
assert app.invoke(None, config, checkpoint_during=checkpoint_during) == {
|
||||
assert app.invoke(None, config, debug=True) == {
|
||||
"my_key": "hi my value here and there and back again",
|
||||
}
|
||||
|
||||
@@ -4333,14 +4060,12 @@ def test_doubly_nested_graph_interrupts(
|
||||
config = {
|
||||
"configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append}
|
||||
}
|
||||
assert [
|
||||
*app.stream({"my_key": "my value"}, config, checkpoint_during=checkpoint_during)
|
||||
] == [
|
||||
assert [*app.stream({"my_key": "my value"}, config)] == [
|
||||
{"parent_1": {"my_key": "hi my value"}},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
assert nodes == ["parent_1", "grandchild_1"]
|
||||
assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [
|
||||
assert [*app.stream(None, config)] == [
|
||||
{"child": {"my_key": "hi my value here and there"}},
|
||||
{"parent_2": {"my_key": "hi my value here and there and back again"}},
|
||||
]
|
||||
@@ -4355,22 +4080,11 @@ def test_doubly_nested_graph_interrupts(
|
||||
|
||||
# test stream values w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "3"}}
|
||||
assert [
|
||||
*app.stream(
|
||||
{"my_key": "my value"},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [
|
||||
{"my_key": "my value"},
|
||||
{"my_key": "hi my value"},
|
||||
]
|
||||
assert [
|
||||
*app.stream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [*app.stream(None, config, stream_mode="values")] == [
|
||||
{"my_key": "hi my value"},
|
||||
{"my_key": "hi my value here and there"},
|
||||
{"my_key": "hi my value here and there and back again"},
|
||||
|
||||
@@ -1947,14 +1947,10 @@ async def test_invoke_checkpoint(mocker: MockerFixture, checkpointer_name: str)
|
||||
assert checkpoint["channel_values"].get("total") == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_pending_writes_resume(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
class State(TypedDict):
|
||||
value: Annotated[int, operator.add]
|
||||
|
||||
@@ -1976,12 +1972,10 @@ async def test_pending_writes_resume(
|
||||
self.calls = 0
|
||||
|
||||
one = AwhileMaker(0.1, {"value": 2})
|
||||
two = AwhileMaker(0.2, ConnectionError("I'm not good"))
|
||||
two = AwhileMaker(0.3, ConnectionError("I'm not good"))
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("one", one)
|
||||
builder.add_node(
|
||||
"two", two, retry=RetryPolicy(max_attempts=2, initial_interval=0, jitter=False)
|
||||
)
|
||||
builder.add_node("two", two, retry=RetryPolicy(max_attempts=2))
|
||||
builder.add_edge(START, "one")
|
||||
builder.add_edge(START, "two")
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
@@ -1989,9 +1983,7 @@ async def test_pending_writes_resume(
|
||||
|
||||
thread1: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
||||
with pytest.raises(ConnectionError, match="I'm not good"):
|
||||
await graph.ainvoke(
|
||||
{"value": 1}, thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
await graph.ainvoke({"value": 1}, thread1)
|
||||
|
||||
# both nodes should have been called once
|
||||
assert one.calls == 1
|
||||
@@ -2042,7 +2034,7 @@ async def test_pending_writes_resume(
|
||||
|
||||
# resume execution
|
||||
with pytest.raises(ConnectionError, match="I'm not good"):
|
||||
await graph.ainvoke(None, thread1, checkpoint_during=checkpoint_during)
|
||||
await graph.ainvoke(None, thread1)
|
||||
|
||||
# node "one" succeeded previously, so shouldn't be called again
|
||||
assert one.calls == 1
|
||||
@@ -2056,9 +2048,7 @@ async def test_pending_writes_resume(
|
||||
# resume execution, without exception
|
||||
two.rtn = {"value": 3}
|
||||
# both the pending write and the new write were applied, 1 + 2 + 3 = 6
|
||||
assert await graph.ainvoke(
|
||||
None, thread1, checkpoint_during=checkpoint_during
|
||||
) == {"value": 6}
|
||||
assert await graph.ainvoke(None, thread1) == {"value": 6}
|
||||
|
||||
if "shallow" in checkpointer_name:
|
||||
assert len([c async for c in checkpointer.alist(thread1)]) == 1
|
||||
@@ -2067,7 +2057,7 @@ async def test_pending_writes_resume(
|
||||
# check all final checkpoints
|
||||
checkpoints = [c async for c in checkpointer.alist(thread1)]
|
||||
# we should have 3
|
||||
assert len(checkpoints) == (3 if checkpoint_during else 2)
|
||||
assert len(checkpoints) == 3
|
||||
# the last one not too interesting for this test
|
||||
assert checkpoints[0] == CheckpointTuple(
|
||||
config={
|
||||
@@ -2173,26 +2163,15 @@ async def test_pending_writes_resume(
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": checkpoints[2].config["configurable"][
|
||||
"checkpoint_id"
|
||||
]
|
||||
if checkpoint_during
|
||||
else AnyStr(),
|
||||
],
|
||||
}
|
||||
},
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "value", 3),
|
||||
)
|
||||
if checkpoint_during
|
||||
else UnsortedSequence(
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
# the write against the previous checkpoint is not saved, as it is
|
||||
# produced in a run where only the next checkpoint (the last) is saved
|
||||
),
|
||||
)
|
||||
if not checkpoint_during:
|
||||
return
|
||||
assert checkpoints[2] == CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
@@ -2230,7 +2209,7 @@ async def test_pending_writes_resume(
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_run_from_checkpoint_id_retains_previous_writes(
|
||||
checkpointer_name: str,
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
) -> None:
|
||||
class MyState(TypedDict):
|
||||
myval: Annotated[int, operator.add]
|
||||
@@ -2275,8 +2254,8 @@ async def test_run_from_checkpoint_id_retains_previous_writes(
|
||||
history = [c async for c in graph.aget_state_history(thread1)]
|
||||
|
||||
assert len(history) == 4
|
||||
assert history[0].values == {"myval": 4, "otherval": False}
|
||||
assert history[-1].values == {"myval": 0}
|
||||
assert history[0].values == {"myval": 4, "otherval": False}
|
||||
|
||||
second_run_config = {
|
||||
**thread1,
|
||||
@@ -2453,12 +2432,8 @@ async def test_send_sequences(checkpointer_name: str) -> None:
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
async def test_imp_task(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
mapper_calls = 0
|
||||
|
||||
@@ -2478,12 +2453,7 @@ async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None
|
||||
|
||||
tracer = FakeTracer()
|
||||
thread1 = {"configurable": {"thread_id": "1"}, "callbacks": [tracer]}
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(
|
||||
[0, 1], thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in graph.astream([0, 1], thread1)] == [
|
||||
{"mapper": "00"},
|
||||
{"mapper": "11"},
|
||||
{
|
||||
@@ -2507,9 +2477,7 @@ async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None
|
||||
assert any(r.inputs == {"input": 0} for r in mapper_runs)
|
||||
assert any(r.inputs == {"input": 1} for r in mapper_runs)
|
||||
|
||||
assert await graph.ainvoke(
|
||||
Command(resume="answer"), thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert await graph.ainvoke(Command(resume="answer"), thread1) == [
|
||||
"00answer",
|
||||
"11answer",
|
||||
]
|
||||
@@ -2517,12 +2485,8 @@ async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_nested(checkpointer_name: str, checkpoint_during: bool) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
async def test_imp_nested(checkpointer_name: str) -> None:
|
||||
async def mynode(input: list[str]) -> list[str]:
|
||||
return [it + "a" for it in input]
|
||||
|
||||
@@ -2562,12 +2526,7 @@ async def test_imp_nested(checkpointer_name: str, checkpoint_during: bool) -> No
|
||||
}
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(
|
||||
[0, 1], thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in graph.astream([0, 1], thread1)] == [
|
||||
{"submapper": "0"},
|
||||
{"mapper": "00"},
|
||||
{"submapper": "1"},
|
||||
@@ -2584,21 +2543,15 @@ async def test_imp_nested(checkpointer_name: str, checkpoint_during: bool) -> No
|
||||
},
|
||||
]
|
||||
|
||||
assert await graph.ainvoke(
|
||||
Command(resume="answer"), thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert await graph.ainvoke(Command(resume="answer"), thread1) == [
|
||||
"00answera",
|
||||
"11answera",
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_task_cancel(checkpointer_name: str, checkpoint_during: bool) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
async def test_imp_task_cancel(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
mapper_calls = 0
|
||||
mapper_cancels = 0
|
||||
@@ -2624,12 +2577,7 @@ async def test_imp_task_cancel(checkpointer_name: str, checkpoint_during: bool)
|
||||
return [m + answer for m in mapped]
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(
|
||||
[0, 1], thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in graph.astream([0, 1], thread1)] == [
|
||||
{"mapper": "00"},
|
||||
{
|
||||
"__interrupt__": (
|
||||
@@ -2645,9 +2593,7 @@ async def test_imp_task_cancel(checkpointer_name: str, checkpoint_during: bool)
|
||||
assert mapper_calls == 2
|
||||
assert mapper_cancels == 1
|
||||
|
||||
assert await graph.ainvoke(
|
||||
Command(resume="answer"), thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert await graph.ainvoke(Command(resume="answer"), thread1) == [
|
||||
"00answer",
|
||||
]
|
||||
assert mapper_calls == 3
|
||||
@@ -2655,14 +2601,8 @@ async def test_imp_task_cancel(checkpointer_name: str, checkpoint_during: bool)
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_sync_from_async(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
async def test_imp_sync_from_async(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
@task()
|
||||
@@ -2685,12 +2625,7 @@ async def test_imp_sync_from_async(
|
||||
return fut_baz.result()
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(
|
||||
{"a": "0"}, thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in graph.astream({"a": "0"}, thread1)] == [
|
||||
{"foo": {"a": "0foo", "b": "bar"}},
|
||||
{"bar": {"a": "0foobar", "c": "bark"}},
|
||||
{"baz": {"a": "0foobarbaz", "c": "something else"}},
|
||||
@@ -2699,14 +2634,8 @@ async def test_imp_sync_from_async(
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_stream_order(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
if not checkpoint_during and "shallow" in checkpointer_name:
|
||||
pytest.skip("Checkpointing during execution not supported")
|
||||
|
||||
async def test_imp_stream_order(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
@task()
|
||||
@@ -2730,12 +2659,7 @@ async def test_imp_stream_order(
|
||||
return await fut_baz
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [
|
||||
c
|
||||
async for c in graph.astream(
|
||||
{"a": "0"}, thread1, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in graph.astream({"a": "0"}, thread1)] == [
|
||||
{"foo": {"a": "0foo", "b": "bar"}},
|
||||
{"bar": {"a": "0foobar", "c": "bark"}},
|
||||
{"baz": {"a": "0foobarbaz", "c": "something else"}},
|
||||
@@ -2743,11 +2667,8 @@ async def test_imp_stream_order(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_send_dedupe_on_resume(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
|
||||
class InterruptOnce:
|
||||
ticks: int = 0
|
||||
|
||||
@@ -2798,9 +2719,7 @@ async def test_send_dedupe_on_resume(
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert await graph.ainvoke(
|
||||
["0"], thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert await graph.ainvoke(["0"], thread1, debug=1) == [
|
||||
"0",
|
||||
"1",
|
||||
"3.1",
|
||||
@@ -2812,9 +2731,7 @@ async def test_send_dedupe_on_resume(
|
||||
assert builder.nodes["2"].runnable.func.ticks == 3
|
||||
assert builder.nodes["flaky"].runnable.func.ticks == 1
|
||||
# resume execution
|
||||
assert await graph.ainvoke(
|
||||
None, thread1, checkpoint_during=checkpoint_during
|
||||
) == [
|
||||
assert await graph.ainvoke(None, thread1, debug=1) == [
|
||||
"0",
|
||||
"1",
|
||||
"3.1",
|
||||
@@ -2831,8 +2748,7 @@ async def test_send_dedupe_on_resume(
|
||||
assert builder.nodes["flaky"].runnable.func.ticks == 2
|
||||
# check history
|
||||
history = [c async for c in graph.aget_state_history(thread1)]
|
||||
assert len(history) == (6 if checkpoint_during else 2)
|
||||
expected_history = [
|
||||
assert history == [
|
||||
StateSnapshot(
|
||||
values=[
|
||||
"0",
|
||||
@@ -2968,9 +2884,13 @@ async def test_send_dedupe_on_resume(
|
||||
name="flaky",
|
||||
path=("__pregel_push", 1),
|
||||
error=None,
|
||||
interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),),
|
||||
interrupts=(
|
||||
Interrupt(
|
||||
value="Bahh", resumable=False, ns=None, when="during"
|
||||
),
|
||||
),
|
||||
state=None,
|
||||
result=["flaky|4"] if checkpoint_during else None,
|
||||
result=["flaky|4"],
|
||||
),
|
||||
PregelTask(
|
||||
id=AnyStr(),
|
||||
@@ -3107,11 +3027,6 @@ async def test_send_dedupe_on_resume(
|
||||
),
|
||||
),
|
||||
]
|
||||
if checkpoint_during:
|
||||
assert history == expected_history
|
||||
else:
|
||||
assert history[0] == expected_history[0]
|
||||
assert history[1] == expected_history[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
@@ -4638,7 +4553,6 @@ async def test_nested_pydantic_models(version: str) -> None:
|
||||
optional_nested: Optional[NestedModel] = None
|
||||
dict_nested: dict[str, NestedModel]
|
||||
my_set: set[int]
|
||||
another_set: set
|
||||
my_enum: MyEnum
|
||||
list_nested: Annotated[
|
||||
Union[dict, list[dict[str, NestedModel]]], lambda x, y: (x or []) + [y]
|
||||
@@ -4667,7 +4581,6 @@ async def test_nested_pydantic_models(version: str) -> None:
|
||||
"nested": {"value": 42, "name": "test"},
|
||||
"optional_nested": {"value": 10, "name": "optional"},
|
||||
"my_set": [1, 2, 7],
|
||||
"another_set": ["foo", 3],
|
||||
"my_enum": MyEnum.B,
|
||||
"my_typed_dict": {"x": 1, "my_enum": MyEnum.A},
|
||||
"dict_nested": {"a": {"value": 5, "name": "a"}},
|
||||
@@ -5435,132 +5348,6 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None:
|
||||
assert times_called == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_subgraph_checkpoint_true(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
class InnerState(TypedDict):
|
||||
my_key: Annotated[str, operator.add]
|
||||
my_other_key: str
|
||||
|
||||
def inner_1(state: InnerState):
|
||||
return {"my_key": " got here", "my_other_key": state["my_key"]}
|
||||
|
||||
def inner_2(state: InnerState):
|
||||
return {"my_key": " and there"}
|
||||
|
||||
inner = StateGraph(InnerState)
|
||||
inner.add_node("inner_1", inner_1)
|
||||
inner.add_node("inner_2", inner_2)
|
||||
inner.add_edge("inner_1", "inner_2")
|
||||
inner.set_entry_point("inner_1")
|
||||
inner.set_finish_point("inner_2")
|
||||
|
||||
class State(TypedDict):
|
||||
my_key: str
|
||||
|
||||
graph = StateGraph(State)
|
||||
graph.add_node("inner", inner.compile(checkpointer=True))
|
||||
graph.add_edge(START, "inner")
|
||||
graph.add_conditional_edges(
|
||||
"inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END
|
||||
)
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
app = graph.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
subgraphs=True,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
] == [
|
||||
(("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}),
|
||||
(("inner",), {"inner_2": {"my_key": " and there"}}),
|
||||
((), {"inner": {"my_key": " got here and there"}}),
|
||||
(
|
||||
("inner",),
|
||||
{
|
||||
"inner_1": {
|
||||
"my_key": " got here",
|
||||
"my_other_key": " got here and there got here and there",
|
||||
}
|
||||
},
|
||||
),
|
||||
(("inner",), {"inner_2": {"my_key": " and there"}}),
|
||||
(
|
||||
(),
|
||||
{
|
||||
"inner": {
|
||||
"my_key": " got here and there got here and there got here and there"
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC)
|
||||
async def test_subgraph_checkpoint_true_interrupt(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
# Define subgraph
|
||||
class SubgraphState(TypedDict):
|
||||
# note that none of these keys are shared with the parent graph state
|
||||
bar: str
|
||||
baz: str
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
baz_value = interrupt("Provide baz value")
|
||||
return {"baz": baz_value}
|
||||
|
||||
def subgraph_node_2(state: SubgraphState):
|
||||
return {"bar": state["bar"] + state["baz"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_node(subgraph_node_2)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
|
||||
subgraph = subgraph_builder.compile(checkpointer=True)
|
||||
|
||||
class ParentState(TypedDict):
|
||||
foo: str
|
||||
|
||||
def node_1(state: ParentState):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
async def node_2(state: ParentState, config: RunnableConfig):
|
||||
response = await subgraph.ainvoke({"bar": state["foo"]})
|
||||
return {"foo": response["bar"]}
|
||||
|
||||
builder = StateGraph(ParentState)
|
||||
builder.add_node("node_1", node_1)
|
||||
builder.add_node("node_2", node_2)
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert await graph.ainvoke(
|
||||
{"foo": "foo"}, config, checkpoint_during=checkpoint_during
|
||||
) == {"foo": "hi! foo"}
|
||||
assert (await graph.aget_state(config, subgraphs=True)).tasks[
|
||||
0
|
||||
].state.values == {"bar": "hi! foo"}
|
||||
assert await graph.ainvoke(
|
||||
Command(resume="baz"), config, checkpoint_during=checkpoint_during
|
||||
) == {"foo": "hi! foobaz"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_stream_subgraphs_during_execution(checkpointer_name: str) -> None:
|
||||
class InnerState(TypedDict):
|
||||
@@ -5669,11 +5456,8 @@ async def test_stream_buffering_single_node(checkpointer_name: str) -> None:
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_nested_graph_interrupts_parallel(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None:
|
||||
class InnerState(TypedDict):
|
||||
my_key: Annotated[str, operator.add]
|
||||
my_other_key: str
|
||||
@@ -5722,13 +5506,11 @@ async def test_nested_graph_interrupts_parallel(
|
||||
|
||||
# test invoke w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await app.ainvoke(
|
||||
{"my_key": ""}, config, checkpoint_during=checkpoint_during
|
||||
) == {
|
||||
assert await app.ainvoke({"my_key": ""}, config, debug=True) == {
|
||||
"my_key": " and parallel",
|
||||
}
|
||||
|
||||
assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == {
|
||||
assert await app.ainvoke(None, config, debug=True) == {
|
||||
"my_key": "got here and there and parallel and back again",
|
||||
}
|
||||
|
||||
@@ -5738,13 +5520,7 @@ async def test_nested_graph_interrupts_parallel(
|
||||
# test stream updates w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
subgraphs=True,
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
c async for c in app.astream({"my_key": ""}, config, subgraphs=True)
|
||||
] == [
|
||||
# we got to parallel node first
|
||||
((), {"outer_1": {"my_key": " and parallel"}}),
|
||||
@@ -5754,12 +5530,7 @@ async def test_nested_graph_interrupts_parallel(
|
||||
),
|
||||
((), {"__interrupt__": ()}),
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config)] == [
|
||||
{"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}},
|
||||
{"inner": {"my_key": "got here and there"}},
|
||||
{"outer_2": {"my_key": " and back again"}},
|
||||
@@ -5768,23 +5539,12 @@ async def test_nested_graph_interrupts_parallel(
|
||||
# test stream values w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "3"}}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
c async for c in app.astream({"my_key": ""}, config, stream_mode="values")
|
||||
] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
@@ -5794,32 +5554,16 @@ async def test_nested_graph_interrupts_parallel(
|
||||
app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"])
|
||||
config = {"configurable": {"thread_id": "4"}}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
c async for c in app.astream({"my_key": ""}, config, stream_mode="values")
|
||||
] == [
|
||||
{"my_key": ""},
|
||||
]
|
||||
# while we're waiting for the node w/ interrupt inside to finish
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
@@ -5829,42 +5573,23 @@ async def test_nested_graph_interrupts_parallel(
|
||||
app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"])
|
||||
config = {"configurable": {"thread_id": "5"}}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": ""},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
)
|
||||
c async for c in app.astream({"my_key": ""}, config, stream_mode="values")
|
||||
] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": " and parallel"},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": ""},
|
||||
{"my_key": "got here and there and parallel"},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": "got here and there and parallel"},
|
||||
{"my_key": "got here and there and parallel and back again"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint_during", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_doubly_nested_graph_interrupts(
|
||||
checkpointer_name: str, checkpoint_during: bool
|
||||
) -> None:
|
||||
async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None:
|
||||
class State(TypedDict):
|
||||
my_key: str
|
||||
|
||||
@@ -5917,13 +5642,11 @@ async def test_doubly_nested_graph_interrupts(
|
||||
|
||||
# test invoke w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await app.ainvoke(
|
||||
{"my_key": "my value"}, config, checkpoint_during=checkpoint_during
|
||||
) == {
|
||||
assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == {
|
||||
"my_key": "hi my value",
|
||||
}
|
||||
|
||||
assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == {
|
||||
assert await app.ainvoke(None, config, debug=True) == {
|
||||
"my_key": "hi my value here and there and back again",
|
||||
}
|
||||
|
||||
@@ -5932,22 +5655,12 @@ async def test_doubly_nested_graph_interrupts(
|
||||
config = {
|
||||
"configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append}
|
||||
}
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": "my value"}, config, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream({"my_key": "my value"}, config)] == [
|
||||
{"parent_1": {"my_key": "hi my value"}},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
assert nodes == ["parent_1", "grandchild_1"]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config)] == [
|
||||
{"child": {"my_key": "hi my value here and there"}},
|
||||
{"parent_2": {"my_key": "hi my value here and there and back again"}},
|
||||
]
|
||||
@@ -5965,21 +5678,13 @@ async def test_doubly_nested_graph_interrupts(
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
{"my_key": "my value"},
|
||||
config,
|
||||
stream_mode="values",
|
||||
checkpoint_during=checkpoint_during,
|
||||
{"my_key": "my value"}, config, stream_mode="values"
|
||||
)
|
||||
] == [
|
||||
{"my_key": "my value"},
|
||||
{"my_key": "hi my value"},
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in app.astream(
|
||||
None, config, stream_mode="values", checkpoint_during=checkpoint_during
|
||||
)
|
||||
] == [
|
||||
assert [c async for c in app.astream(None, config, stream_mode="values")] == [
|
||||
{"my_key": "hi my value"},
|
||||
{"my_key": "hi my value here and there"},
|
||||
{"my_key": "hi my value here and there and back again"},
|
||||
|
||||
@@ -6,10 +6,6 @@ client.cjs
|
||||
client.js
|
||||
client.d.ts
|
||||
client.d.cts
|
||||
auth.cjs
|
||||
auth.js
|
||||
auth.d.ts
|
||||
auth.d.cts
|
||||
react.cjs
|
||||
react.js
|
||||
react.d.ts
|
||||
|
||||
@@ -14,7 +14,6 @@ export const config = {
|
||||
entrypoints: {
|
||||
index: "index",
|
||||
client: "client",
|
||||
auth: "auth/index",
|
||||
react: "react/index",
|
||||
"react-ui": "react-ui/index",
|
||||
"react-ui/server": "react-ui/server/index",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.66",
|
||||
"version": "0.0.63",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
@@ -72,15 +72,6 @@
|
||||
"import": "./client.js",
|
||||
"require": "./client.cjs"
|
||||
},
|
||||
"./auth": {
|
||||
"types": {
|
||||
"import": "./auth.d.ts",
|
||||
"require": "./auth.d.cts",
|
||||
"default": "./auth.d.ts"
|
||||
},
|
||||
"import": "./auth.js",
|
||||
"require": "./auth.cjs"
|
||||
},
|
||||
"./react": {
|
||||
"types": {
|
||||
"import": "./react.d.ts",
|
||||
@@ -120,10 +111,6 @@
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"client.d.cts",
|
||||
"auth.cjs",
|
||||
"auth.js",
|
||||
"auth.d.ts",
|
||||
"auth.d.cts",
|
||||
"react.cjs",
|
||||
"react.js",
|
||||
"react.d.ts",
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
const HTTP_STATUS_MAPPING: { [key: number]: string } = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
102: "Processing",
|
||||
103: "Early Hints",
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
203: "Non-Authoritative Information",
|
||||
204: "No Content",
|
||||
205: "Reset Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
208: "Already Reported",
|
||||
226: "IM Used",
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
303: "See Other",
|
||||
304: "Not Modified",
|
||||
305: "Use Proxy",
|
||||
307: "Temporary Redirect",
|
||||
308: "Permanent Redirect",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Request Entity Too Large",
|
||||
414: "Request-URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Requested Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "I'm a Teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Entity",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
};
|
||||
|
||||
export class HTTPException extends Error {
|
||||
status: number;
|
||||
headers: HeadersInit;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
options?: { message?: string; headers?: HeadersInit; cause?: unknown },
|
||||
) {
|
||||
super(options?.message ?? HTTP_STATUS_MAPPING[status] ?? "Unknown error", {
|
||||
cause: options?.cause,
|
||||
});
|
||||
this.status = status;
|
||||
this.headers = options?.headers ?? {};
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type {
|
||||
AuthenticateCallback,
|
||||
AnyCallback,
|
||||
CallbackEvent,
|
||||
OnCallback,
|
||||
BaseAuthReturn,
|
||||
ToUserLike,
|
||||
BaseUser,
|
||||
} from "./types.js";
|
||||
|
||||
export class Auth<
|
||||
TExtra = {},
|
||||
TAuthReturn extends BaseAuthReturn = BaseAuthReturn,
|
||||
TUser extends BaseUser = ToUserLike<TAuthReturn>,
|
||||
> {
|
||||
"~handlerCache": {
|
||||
authenticate?: AuthenticateCallback<BaseAuthReturn>;
|
||||
callbacks?: Record<string, AnyCallback>;
|
||||
} = {};
|
||||
|
||||
authenticate<T extends BaseAuthReturn>(
|
||||
cb: AuthenticateCallback<T>,
|
||||
): Auth<TExtra, T> {
|
||||
this["~handlerCache"].authenticate = cb;
|
||||
return this as unknown as Auth<TExtra, T>;
|
||||
}
|
||||
|
||||
on<T extends CallbackEvent>(event: T, callback: OnCallback<T, TUser>): this {
|
||||
this["~handlerCache"].callbacks ??= {};
|
||||
const events: string[] = Array.isArray(event) ? event : [event];
|
||||
for (const event of events) {
|
||||
this["~handlerCache"].callbacks[event] = callback as AnyCallback;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Filters as AuthFilters,
|
||||
EventValueMap as AuthEventValueMap,
|
||||
} from "./types.js";
|
||||
export { HTTPException } from "./error.js";
|
||||
@@ -1,348 +0,0 @@
|
||||
type Maybe<T> = T | null | undefined;
|
||||
type PromiseMaybe<T> = Promise<T> | T;
|
||||
|
||||
interface AssistantConfig {
|
||||
tags?: Maybe<string[]>;
|
||||
recursion_limit?: Maybe<number>;
|
||||
configurable?: Maybe<{
|
||||
thread_id?: Maybe<string>;
|
||||
thread_ts?: Maybe<string>;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface AssistantCreate {
|
||||
assistant_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
config?: Maybe<AssistantConfig>;
|
||||
if_exists?: Maybe<"raise" | "do_nothing">;
|
||||
name?: Maybe<string>;
|
||||
graph_id: string;
|
||||
}
|
||||
|
||||
interface AssistantRead {
|
||||
assistant_id: string;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface AssistantUpdate {
|
||||
assistant_id: string;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
config?: Maybe<AssistantConfig>;
|
||||
graph_id?: Maybe<string>;
|
||||
name?: Maybe<string>;
|
||||
version?: Maybe<number>;
|
||||
}
|
||||
|
||||
interface AssistantDelete {
|
||||
assistant_id: string;
|
||||
}
|
||||
|
||||
interface AssistantSearch {
|
||||
graph_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
interface ThreadCreate {
|
||||
thread_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
if_exists?: Maybe<"raise" | "do_nothing">;
|
||||
}
|
||||
|
||||
interface ThreadRead {
|
||||
thread_id?: Maybe<string>;
|
||||
}
|
||||
|
||||
interface ThreadUpdate {
|
||||
thread_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
action?: Maybe<"interrupt" | "rollback">;
|
||||
}
|
||||
|
||||
interface ThreadDelete {
|
||||
thread_id?: Maybe<string>;
|
||||
run_id?: Maybe<string>;
|
||||
}
|
||||
|
||||
interface ThreadSearch {
|
||||
thread_id?: Maybe<string>;
|
||||
status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
values?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
interface CronCreate {
|
||||
payload?: Maybe<Record<string, unknown>>;
|
||||
schedule: string;
|
||||
cron_id?: Maybe<string>;
|
||||
thread_id?: Maybe<string>;
|
||||
user_id?: Maybe<string>;
|
||||
end_time?: Maybe<string>;
|
||||
}
|
||||
|
||||
interface CronRead {
|
||||
cron_id: string;
|
||||
}
|
||||
|
||||
interface CronUpdate {
|
||||
cron_id: string;
|
||||
payload?: Maybe<Record<string, unknown>>;
|
||||
schedule?: Maybe<string>;
|
||||
}
|
||||
|
||||
interface CronDelete {
|
||||
cron_id: string;
|
||||
}
|
||||
|
||||
interface CronSearch {
|
||||
assistant_id?: Maybe<string>;
|
||||
thread_id?: Maybe<string>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
interface StorePut {
|
||||
namespace: string[];
|
||||
key: string;
|
||||
value: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface StoreGet {
|
||||
namespace: Maybe<string[]>;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface StoreSearch {
|
||||
namespace?: Maybe<string[]>;
|
||||
filter?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
query?: Maybe<string>;
|
||||
}
|
||||
|
||||
interface StoreListNamespaces {
|
||||
namespace?: Maybe<string[]>;
|
||||
suffix?: Maybe<string[]>;
|
||||
max_depth?: Maybe<number>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
interface StoreDelete {
|
||||
namespace?: Maybe<string[]>;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface RunsCreate {
|
||||
thread_id?: Maybe<string>;
|
||||
assistant_id: string;
|
||||
run_id: string;
|
||||
status: Maybe<
|
||||
"pending" | "running" | "error" | "success" | "timeout" | "interrupted"
|
||||
>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
prevent_insert_if_inflight?: Maybe<boolean>;
|
||||
multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">;
|
||||
if_not_exists?: Maybe<"reject" | "create">;
|
||||
after_seconds?: Maybe<number>;
|
||||
kwargs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EventValueMap {
|
||||
["threads:create"]: ThreadCreate;
|
||||
["threads:read"]: ThreadRead;
|
||||
["threads:update"]: ThreadUpdate;
|
||||
["threads:delete"]: ThreadDelete;
|
||||
["threads:search"]: ThreadSearch;
|
||||
["threads:create_run"]: RunsCreate;
|
||||
|
||||
["assistants:create"]: AssistantCreate;
|
||||
["assistants:read"]: AssistantRead;
|
||||
["assistants:update"]: AssistantUpdate;
|
||||
["assistants:delete"]: AssistantDelete;
|
||||
["assistants:search"]: AssistantSearch;
|
||||
|
||||
["crons:create"]: CronCreate;
|
||||
["crons:read"]: CronRead;
|
||||
["crons:update"]: CronUpdate;
|
||||
["crons:delete"]: CronDelete;
|
||||
["crons:search"]: CronSearch;
|
||||
|
||||
["store:put"]: StorePut;
|
||||
["store:get"]: StoreGet;
|
||||
["store:search"]: StoreSearch;
|
||||
["store:list_namespaces"]: StoreListNamespaces;
|
||||
["store:delete"]: StoreDelete;
|
||||
}
|
||||
interface ResourceType {
|
||||
threads:
|
||||
| "threads:create"
|
||||
| "threads:read"
|
||||
| "threads:update"
|
||||
| "threads:delete"
|
||||
| "threads:search"
|
||||
| "threads:create_run";
|
||||
|
||||
assistants:
|
||||
| "assistants:create"
|
||||
| "assistants:read"
|
||||
| "assistants:update"
|
||||
| "assistants:delete"
|
||||
| "assistants:search";
|
||||
crons:
|
||||
| "crons:create"
|
||||
| "crons:read"
|
||||
| "crons:update"
|
||||
| "crons:delete"
|
||||
| "crons:search";
|
||||
|
||||
store:
|
||||
| "store:put"
|
||||
| "store:get"
|
||||
| "store:search"
|
||||
| "store:list_namespaces"
|
||||
| "store:delete";
|
||||
}
|
||||
interface ActionType {
|
||||
"*:create": "threads:create" | "assistants:create" | "crons:create";
|
||||
|
||||
"*:read": "threads:read" | "assistants:read" | "crons:read";
|
||||
|
||||
"*:update": "threads:update" | "assistants:update" | "crons:update";
|
||||
|
||||
"*:delete":
|
||||
| "threads:delete"
|
||||
| "assistants:delete"
|
||||
| "crons:delete"
|
||||
| "store:delete";
|
||||
|
||||
"*:search":
|
||||
| "threads:search"
|
||||
| "assistants:search"
|
||||
| "crons:search"
|
||||
| "store:search";
|
||||
|
||||
"*:create_run": "threads:create_run";
|
||||
|
||||
"*:put": "store:put";
|
||||
|
||||
"*:get": "store:get";
|
||||
|
||||
"*:list_namespaces": "store:list_namespaces";
|
||||
}
|
||||
|
||||
export type BaseAuthReturn =
|
||||
| {
|
||||
is_authenticated?: boolean;
|
||||
display_name?: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
| string;
|
||||
|
||||
export interface BaseUser {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export type ToUserLike<T extends BaseAuthReturn> = T extends string
|
||||
? {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
: Omit<T, "is_authenticated" | "display_name"> & {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
};
|
||||
|
||||
type CallbackParameter<
|
||||
Event extends string = string,
|
||||
Resource extends string = string,
|
||||
Action extends string = string,
|
||||
Value extends unknown = unknown,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = {
|
||||
event: Event;
|
||||
resource: Resource;
|
||||
action: Action;
|
||||
value: Value;
|
||||
user: TUser;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
type ContextMap = {
|
||||
[EventType in keyof EventValueMap]: CallbackParameter<
|
||||
EventType,
|
||||
EventType extends `${infer Resource}:${string}` ? Resource : never,
|
||||
EventType extends `${string}:${infer Action}` ? Action : never,
|
||||
EventValueMap[EventType],
|
||||
BaseUser
|
||||
>;
|
||||
};
|
||||
|
||||
type ActionCallbackParameter<
|
||||
T extends keyof ActionType,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[ActionType[T]] & { user: TUser };
|
||||
type AuthCallbackParameter<
|
||||
T extends keyof EventValueMap,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[T] & { user: TUser };
|
||||
type ResourceCallbackParameter<
|
||||
T extends keyof ResourceType,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[ResourceType[T]] & { user: TUser };
|
||||
|
||||
export type Filters<TKey extends string | number | symbol> = {
|
||||
[key in TKey]: string | { [op in "$contains" | "$eq"]?: string };
|
||||
};
|
||||
|
||||
export interface AuthenticateCallback<T extends BaseAuthReturn> {
|
||||
(request: Request): PromiseMaybe<T>;
|
||||
}
|
||||
|
||||
type OnKey = keyof ResourceType | keyof ActionType | keyof EventValueMap;
|
||||
|
||||
type OnSingleParameter<
|
||||
T extends OnKey,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = T extends keyof ResourceType
|
||||
? ResourceCallbackParameter<T, TUser>
|
||||
: T extends keyof ActionType
|
||||
? ActionCallbackParameter<T, TUser>
|
||||
: T extends keyof EventValueMap
|
||||
? AuthCallbackParameter<T, TUser>
|
||||
: never;
|
||||
|
||||
type OnParameter<
|
||||
T extends "*" | OnKey | OnKey[],
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = T extends OnKey[]
|
||||
? OnSingleParameter<T[number], TUser>
|
||||
: T extends "*"
|
||||
? AuthCallbackParameter<keyof EventValueMap, TUser>
|
||||
: T extends OnKey
|
||||
? OnSingleParameter<T, TUser>
|
||||
: never;
|
||||
|
||||
export type AnyCallback = (
|
||||
request: CallbackParameter,
|
||||
) => void | boolean | Filters<string>;
|
||||
|
||||
export type CallbackEvent = "*" | OnKey | OnKey[];
|
||||
|
||||
export type OnCallback<
|
||||
T extends CallbackEvent,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = (
|
||||
request: OnParameter<T, TUser>,
|
||||
) => void | boolean | Filters<keyof TMetadata>;
|
||||
@@ -12,9 +12,11 @@ const STATUS_NO_RETRY = [
|
||||
406, // Not Acceptable
|
||||
407, // Proxy Authentication Required
|
||||
408, // Request Timeout
|
||||
409, // Conflict
|
||||
422, // Unprocessable Entity
|
||||
];
|
||||
const STATUS_IGNORE = [
|
||||
409, // Conflict
|
||||
];
|
||||
|
||||
type ResponseCallback = (response?: Response) => Promise<boolean>;
|
||||
|
||||
@@ -169,6 +171,8 @@ export class AsyncCaller {
|
||||
if (error instanceof HTTPError) {
|
||||
if (STATUS_NO_RETRY.includes(error.status)) {
|
||||
throw error;
|
||||
} else if (STATUS_IGNORE.includes(error.status)) {
|
||||
return;
|
||||
}
|
||||
if (onFailedResponseHook && error.response) {
|
||||
await onFailedResponseHook(error.response);
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "ES2022.Error", "DOM"],
|
||||
"lib": [
|
||||
"ES2021",
|
||||
"ES2022.Object",
|
||||
"DOM"
|
||||
],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
@@ -18,14 +22,24 @@
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "coverage"],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage"
|
||||
],
|
||||
"includeVersion": true,
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/client.ts"],
|
||||
"entryPoints": [
|
||||
"src/client.ts"
|
||||
],
|
||||
"readme": "none",
|
||||
"out": "docs",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"plugin": [
|
||||
"typedoc-plugin-markdown"
|
||||
],
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeExternals": false
|
||||
|
||||
Reference in New Issue
Block a user