mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-27 01:52:25 +02:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb953ddd4 | ||
|
|
5ddfce1814 | ||
|
|
1f31e0b9b6 | ||
|
|
1b37ece92f | ||
|
|
e2e90da5dc | ||
|
|
d542d8aecb | ||
|
|
a04ec5d6f0 | ||
|
|
50df7d423a | ||
|
|
c4a4a46473 | ||
|
|
f178eb821e | ||
|
|
48167d7fec | ||
|
|
806878a421 | ||
|
|
8087e6a42c | ||
|
|
8fbdb14487 | ||
|
|
5093802f31 | ||
|
|
b89ef60b91 | ||
|
|
672da815a3 | ||
|
|
b704e41632 | ||
|
|
ea7906b177 | ||
|
|
ef7897e12e | ||
|
|
e9fed2798e | ||
|
|
ed293f16d6 | ||
|
|
f702729e04 | ||
|
|
b0f14649e0 | ||
|
|
ea20432b9b | ||
|
|
e2efab8061 | ||
|
|
9babffa054 | ||
|
|
73cebea3c2 | ||
|
|
b73b2d19eb | ||
|
|
ca26805b5f | ||
|
|
5ac837d7cd | ||
|
|
c4f5861166 | ||
|
|
172238b2d5 | ||
|
|
095da17833 | ||
|
|
e931c68669 | ||
|
|
666c224c2d | ||
|
|
21a6f41e0a |
@@ -31,6 +31,8 @@ jobs:
|
||||
workdir: libs/cli/examples/graphs_reqs_b
|
||||
tag: langgraph-test-d
|
||||
name: "CLI integration test"
|
||||
env:
|
||||
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: libs/cli
|
||||
@@ -38,11 +40,12 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
filter: "libs/cli/**"
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.changed-files.outputs.all
|
||||
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -50,15 +53,15 @@ jobs:
|
||||
cache-suffix: "cli-integration-test"
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Install cli globally
|
||||
if: steps.changed-files.outputs.all
|
||||
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
||||
run: pip install -e .
|
||||
- name: Build service ${{ matrix.example.name }}
|
||||
if: steps.changed-files.outputs.all
|
||||
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
run: |
|
||||
langgraph build -t ${{ matrix.example.tag }}
|
||||
- name: Test service ${{ matrix.example.name }}
|
||||
if: ${{ steps.changed-files.outputs.all && secrets.LANGSMITH_API_KEY != '' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
@@ -72,24 +75,24 @@ jobs:
|
||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
|
||||
|
||||
- name: Build JS service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/js-examples
|
||||
run: |
|
||||
langgraph build -t langgraph-test-e
|
||||
|
||||
- name: Build JS monorepo service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/js-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
|
||||
|
||||
- name: Build Python monorepo service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/python-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
- name: Test Python monorepo service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && secrets.LANGSMITH_API_KEY != '' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/python-monorepo-example
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
@@ -99,12 +102,12 @@ jobs:
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
|
||||
- name: Build prerelease reqs service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||
run: |
|
||||
langgraph build -t langgraph-test-h
|
||||
- name: Test prerelease reqs service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && secrets.LANGSMITH_API_KEY != '' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
@@ -130,7 +133,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and test prerelease reqs fail service
|
||||
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
||||
run: |
|
||||
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
|
||||
|
||||
@@ -34,11 +34,12 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
filter: "${{ inputs.working-directory }}/**"
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -46,12 +47,12 @@ jobs:
|
||||
cache-suffix: lint-${{ inputs.working-directory }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: uv sync --frozen --group lint
|
||||
|
||||
- name: Get .mypy_cache to speed up mypy
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
uses: actions/cache@v5
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
|
||||
@@ -61,7 +62,7 @@ jobs:
|
||||
key: mypy-lint-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
|
||||
|
||||
- name: Analysing package code with our lint
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
if make lint_package > /dev/null 2>&1; then
|
||||
@@ -72,12 +73,12 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Install test dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: uv sync --group lint
|
||||
|
||||
- name: Get .mypy_cache_test to speed up mypy
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
uses: actions/cache@v5
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
|
||||
@@ -87,7 +88,7 @@ jobs:
|
||||
key: mypy-test-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
|
||||
|
||||
- name: Analysing tests with our lint
|
||||
if: steps.changed-files.outputs.all
|
||||
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
if make lint_tests > /dev/null 2>&1; then
|
||||
|
||||
@@ -45,6 +45,13 @@ jobs:
|
||||
shell: bash
|
||||
run: make test_parallel
|
||||
|
||||
- name: Run strict msgpack pregel tests
|
||||
if: ${{ matrix.python-version == '3.13' }}
|
||||
shell: bash
|
||||
env:
|
||||
LANGGRAPH_STRICT_MSGPACK: "true"
|
||||
run: make test TEST="tests/test_pregel.py tests/test_pregel_async.py"
|
||||
|
||||
- name: Ensure the tests did not create any additional files
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,11 +26,12 @@ jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
deps: ${{ steps.filter.outputs.deps }}
|
||||
python: ${{ steps.filter.outputs.python || 'true' }}
|
||||
deps: ${{ steps.filter.outputs.deps || 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Deploy Redirects to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/deploy-redirects.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Generate redirect files
|
||||
run: python docs/generate_redirects.py
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: 'docs/_site'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1 @@
|
||||
_site/
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate HTML redirect files from redirects.json.
|
||||
|
||||
Usage:
|
||||
python generate_redirects.py
|
||||
|
||||
This script reads redirects.json and generates individual HTML files
|
||||
for each redirect path. Each HTML file uses meta refresh (0 delay)
|
||||
which is SEO-friendly and treated similarly to 301 redirects by Google.
|
||||
|
||||
To add new redirects, simply edit redirects.json and re-run this script.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Default fallback URL for any path not in the redirect map
|
||||
DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview"
|
||||
|
||||
HTML_TEMPLATE = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<link rel="canonical" href="{url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url={url}">
|
||||
</head>
|
||||
<body>
|
||||
Redirecting...
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
ROOT_HTML_TEMPLATE = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting to LangGraph Documentation</title>
|
||||
<link rel="canonical" href="{url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url={url}">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Documentation has moved</h1>
|
||||
<p>The LangGraph documentation has moved to <a href="{url}">docs.langchain.com</a>.</p>
|
||||
<p>Redirecting you now...</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CATCHALL_404_TEMPLATE = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting to LangGraph Documentation</title>
|
||||
<link rel="canonical" href="{default_url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>
|
||||
// Catchall redirect for any unmapped paths
|
||||
window.location.replace("{default_url}");
|
||||
</script>
|
||||
<meta http-equiv="refresh" content="0; url={default_url}">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Documentation has moved</h1>
|
||||
<p>The LangGraph documentation has moved to <a href="{default_url}">docs.langchain.com</a>.</p>
|
||||
<p>Redirecting you now...</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def generate_redirects():
|
||||
script_dir = Path(__file__).parent
|
||||
output_dir = script_dir / "_site"
|
||||
|
||||
# Load redirects
|
||||
with open(script_dir / "redirects.json") as f:
|
||||
redirects = json.load(f)
|
||||
|
||||
# Clean output directory
|
||||
if output_dir.exists():
|
||||
import shutil
|
||||
shutil.rmtree(output_dir)
|
||||
output_dir.mkdir(parents=True)
|
||||
|
||||
# Generate individual HTML files for each redirect
|
||||
for old_path, new_url in redirects.items():
|
||||
# Remove leading slash and create directory structure
|
||||
path = old_path.lstrip("/")
|
||||
|
||||
# Check if path has a file extension (e.g., .txt, .xml)
|
||||
# If so, create the file directly instead of a directory with index.html
|
||||
path_obj = Path(path)
|
||||
has_extension = path_obj.suffix and len(path_obj.suffix) <= 5
|
||||
|
||||
if not path:
|
||||
html_path = output_dir / "index.html"
|
||||
elif has_extension:
|
||||
# For files with extensions, create the file directly
|
||||
html_path = output_dir / path
|
||||
else:
|
||||
# For directory-style URLs, create index.html inside
|
||||
html_path = output_dir / path / "index.html"
|
||||
|
||||
# Create parent directories
|
||||
html_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write the redirect HTML
|
||||
html_path.write_text(HTML_TEMPLATE.format(url=new_url))
|
||||
print(f"Created: {html_path}")
|
||||
|
||||
# Create root index.html
|
||||
root_index = output_dir / "index.html"
|
||||
if not root_index.exists():
|
||||
root_index.write_text(ROOT_HTML_TEMPLATE.format(url=DEFAULT_REDIRECT))
|
||||
print(f"Created: {root_index}")
|
||||
|
||||
# Create 404.html for catchall
|
||||
catchall_404 = output_dir / "404.html"
|
||||
catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT))
|
||||
print(f"Created: {catchall_404}")
|
||||
|
||||
# Copy static files (like llms.txt) that can't be redirected via HTML
|
||||
static_files = ["llms.txt"]
|
||||
for static_file in static_files:
|
||||
src = script_dir / static_file
|
||||
if src.exists():
|
||||
dst = output_dir / static_file
|
||||
dst.write_text(src.read_text())
|
||||
print(f"Copied: {dst}")
|
||||
|
||||
print(f"\nGenerated {len(redirects)} redirect files in {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_redirects()
|
||||
@@ -0,0 +1,35 @@
|
||||
# LangGraph
|
||||
|
||||
LangGraph documentation has moved to docs.langchain.com.
|
||||
|
||||
## Overview
|
||||
|
||||
- [LangGraph Overview](https://docs.langchain.com/oss/python/langgraph/overview): Introduction to LangGraph, a library for building stateful, multi-actor applications with LLMs.
|
||||
- [Why LangGraph?](https://docs.langchain.com/oss/python/langgraph/why-langgraph): Motivation for LangGraph and its key features.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- [Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api): Learn how to define state, create nodes, and connect them with edges.
|
||||
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming): Stream outputs from your graph for better UX.
|
||||
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence): Add memory and checkpointing to your graphs.
|
||||
- [Add Memory](https://docs.langchain.com/oss/python/langgraph/add-memory): Implement short-term and long-term memory.
|
||||
- [Workflows & Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents): Build agents and workflows with LangGraph.
|
||||
|
||||
## How-To Guides
|
||||
|
||||
- [Use Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs): Compose graphs using subgraphs.
|
||||
- [Observability](https://docs.langchain.com/oss/python/langgraph/observability): Add tracing and debugging to your graphs.
|
||||
- [Common Errors](https://docs.langchain.com/oss/python/langgraph/common-errors): Troubleshoot common LangGraph errors.
|
||||
|
||||
## Tutorials
|
||||
|
||||
- [Agentic RAG](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Build an agentic RAG system with LangGraph.
|
||||
- [SQL Agent](https://docs.langchain.com/oss/python/langgraph/sql-agent): Create a SQL agent with LangGraph.
|
||||
|
||||
## Reference
|
||||
|
||||
- [API Reference](https://reference.langchain.com/python/langgraph/): Complete API documentation for LangGraph.
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
For deploying LangGraph applications in production, see the [LangSmith documentation](https://docs.langchain.com/langsmith/agent-server).
|
||||
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"/how-tos/stream-values": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/stream-updates": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/streaming-content": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/stream-multiple": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/streaming-tokens-without-langchain": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/streaming-from-final-node": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/streaming-events-from-within-tools-without-langchain": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/state-reducers": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-and-update-state",
|
||||
"/how-tos/sequence": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-a-sequence-of-steps",
|
||||
"/how-tos/branching": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-branches",
|
||||
"/how-tos/recursion-limit": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-and-control-loops",
|
||||
"/how-tos/visualization": "https://docs.langchain.com/oss/python/langgraph/graph-api#visualize-your-graph",
|
||||
"/how-tos/input_output_schema": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-input-and-output-schemas",
|
||||
"/how-tos/pass_private_state": "https://docs.langchain.com/oss/python/langgraph/graph-api#pass-private-state-between-nodes",
|
||||
"/how-tos/state-model": "https://docs.langchain.com/oss/python/langgraph/graph-api#use-pydantic-models-for-graph-state",
|
||||
"/how-tos/map-reduce": "https://docs.langchain.com/oss/python/langgraph/graph-api#map-reduce-and-the-send-api",
|
||||
"/how-tos/command": "https://docs.langchain.com/oss/python/langgraph/graph-api#combine-control-flow-and-state-updates-with-command",
|
||||
"/how-tos/configuration": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-runtime-configuration",
|
||||
"/how-tos/node-retries": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-retry-policies",
|
||||
"/how-tos/return-when-recursion-limit-hits": "https://docs.langchain.com/oss/python/langgraph/graph-api#impose-a-recursion-limit",
|
||||
"/how-tos/async": "https://docs.langchain.com/oss/python/langgraph/graph-api#async",
|
||||
"/how-tos/memory/manage-conversation-history": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/memory/delete-messages": "https://docs.langchain.com/oss/python/langgraph/add-memory#delete-messages",
|
||||
"/how-tos/memory/add-summary-conversation-history": "https://docs.langchain.com/oss/python/langgraph/add-memory#summarize-messages",
|
||||
"/how-tos/memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/agents/memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/subgraph-transform-state": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#different-state-schemas",
|
||||
"/how-tos/subgraphs-manage-state": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#add-persistence",
|
||||
"/how-tos/persistence_postgres": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"/how-tos/persistence_mongodb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"/how-tos/persistence_redis": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"/how-tos/subgraph-persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-with-subgraphs",
|
||||
"/how-tos/cross-thread-persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
|
||||
"/cloud/how-tos/copy_threads": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"/cloud/how-tos/check-thread-status": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"/cloud/concepts/threads": "https://docs.langchain.com/oss/python/langgraph/persistence#threads",
|
||||
"/how-tos/persistence": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/tool-calling-errors": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/how-tos/pass-config-to-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/how-tos/pass-run-time-values-to-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/how-tos/update-state-from-tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/agents/tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/how-tos/agent-handoffs": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/multi-agent-network": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/multi-agent-multi-turn-convo": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/cloud/index": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/cloud/how-tos/index": "https://docs.langchain.com/langsmith/home",
|
||||
"/cloud/concepts/api": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"/cloud/concepts/cloud": "https://docs.langchain.com/langsmith/cloud",
|
||||
"/cloud/faq/studio": "https://docs.langchain.com/langsmith/studio",
|
||||
"/cloud/how-tos/human_in_the_loop_edit_state": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"/cloud/how-tos/human_in_the_loop_user_input": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"/concepts/platform_architecture": "https://docs.langchain.com/langsmith/cloud#architecture",
|
||||
"/cloud/how-tos/stream_values": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/how-tos/stream_updates": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/how-tos/stream_messages": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/how-tos/stream_events": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/how-tos/stream_debug": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/how-tos/stream_multiple": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/concepts/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/agents/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/create-react-agent": "https://docs.langchain.com/oss/python/langchain/agents#basic-configuration",
|
||||
"/how-tos/create-react-agent-memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/create-react-agent-system-prompt": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/create-react-agent-structured-output": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
|
||||
"/prebuilt": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"/reference/prebuilt": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"/concepts/high_level": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/concepts/index": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/concepts/v0-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/how-tos/index": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/introduction": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/agents/deployment": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"/how-tos/deploy-self-hosted": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/concepts/self_hosted": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/tutorials/deployment": "https://docs.langchain.com/langsmith/deployments",
|
||||
"/cloud/how-tos/assistant_versioning": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"/cloud/concepts/runs": "https://docs.langchain.com/langsmith/assistants#execution",
|
||||
"/how-tos/wait-user-input-functional": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"/how-tos/review-tool-calls-functional": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"/how-tos/create-react-agent-hitl": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/agents/human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/how-tos/human_in_the_loop/dynamic_breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/concepts/breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/how-tos/human_in_the_loop/breakpoints": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/cloud/how-tos/human_in_the_loop_breakpoint": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"/how-tos/human_in_the_loop/edit-graph-state": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"/examples/index": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"/guides/index": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"/tutorials/index": "https://docs.langchain.com/oss/python/learn",
|
||||
"/llms-txt-overview": "https://docs.langchain.com/llms.txt",
|
||||
"/tutorials/rag/langgraph_adaptive_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/multi_agent/multi-agent-collaboration": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"/how-tos/create-react-agent-manage-message-history": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/many-tools": "https://docs.langchain.com/oss/python/langchain/tools",
|
||||
"/tutorials/customer-support/customer-support": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/how-tos/react-agent-structured-output": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
|
||||
"/tutorials/code_assistant/langgraph_code_assistant": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/multi_agent/hierarchical_agent_teams": "https://docs.langchain.com/oss/python/langchain/supervisor",
|
||||
"/tutorials/auth/getting_started": "https://docs.langchain.com/langsmith/auth",
|
||||
"/tutorials/auth/resource_auth": "https://docs.langchain.com/langsmith/resource-auth",
|
||||
"/tutorials/auth/add_auth_server": "https://docs.langchain.com/langsmith/add-auth-server",
|
||||
"/how-tos/use-remote-graph": "https://docs.langchain.com/langsmith/use-remote-graph",
|
||||
"/how-tos/autogen-integration": "https://docs.langchain.com/langsmith/autogen-integration",
|
||||
"/how-tos/human_in_the_loop/wait-user-input": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/cloud/how-tos/use_stream_react": "https://docs.langchain.com/langsmith/use-stream-react",
|
||||
"/cloud/how-tos/generative_ui_react": "https://docs.langchain.com/langsmith/generative-ui-react",
|
||||
"/concepts/langgraph_platform": "https://docs.langchain.com/langsmith/home",
|
||||
"/concepts/langgraph_components": "https://docs.langchain.com/langsmith/components",
|
||||
"/concepts/langgraph_server": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"/concepts/langgraph_data_plane": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"/concepts/langgraph_control_plane": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"/concepts/langgraph_cli": "https://docs.langchain.com/langsmith/cli",
|
||||
"/concepts/langgraph_studio": "https://docs.langchain.com/langsmith/studio",
|
||||
"/cloud/how-tos/studio/quick_start": "https://docs.langchain.com/langsmith/quick-start-studio",
|
||||
"/cloud/how-tos/invoke_studio": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"/cloud/how-tos/studio/manage_assistants": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"/cloud/how-tos/threads_studio": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"/cloud/how-tos/iterate_graph_studio": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"/cloud/how-tos/studio/run_evals": "https://docs.langchain.com/langsmith/observability",
|
||||
"/cloud/how-tos/clone_traces_studio": "https://docs.langchain.com/langsmith/observability",
|
||||
"/cloud/how-tos/datasets_studio": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"/concepts/sdk": "https://docs.langchain.com/langsmith/sdk",
|
||||
"/concepts/plans": "https://docs.langchain.com/langsmith/home",
|
||||
"/concepts/application_structure": "https://docs.langchain.com/langsmith/application-structure",
|
||||
"/concepts/scalability_and_resilience": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
||||
"/concepts/auth": "https://docs.langchain.com/langsmith/auth",
|
||||
"/how-tos/auth/custom_auth": "https://docs.langchain.com/langsmith/custom-auth",
|
||||
"/how-tos/auth/openapi_security": "https://docs.langchain.com/langsmith/openapi-security",
|
||||
"/concepts/assistants": "https://docs.langchain.com/langsmith/assistants",
|
||||
"/cloud/how-tos/configuration_cloud": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"/cloud/how-tos/use_threads": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"/cloud/how-tos/background_run": "https://docs.langchain.com/langsmith/background-run",
|
||||
"/cloud/how-tos/same-thread": "https://docs.langchain.com/langsmith/same-thread",
|
||||
"/cloud/how-tos/stateless_runs": "https://docs.langchain.com/langsmith/stateless-runs",
|
||||
"/cloud/how-tos/configurable_headers": "https://docs.langchain.com/langsmith/configurable-headers",
|
||||
"/concepts/double_texting": "https://docs.langchain.com/langsmith/double-texting",
|
||||
"/cloud/how-tos/interrupt_concurrent": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
||||
"/cloud/how-tos/rollback_concurrent": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
||||
"/cloud/how-tos/reject_concurrent": "https://docs.langchain.com/langsmith/reject-concurrent",
|
||||
"/cloud/how-tos/enqueue_concurrent": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
||||
"/cloud/concepts/webhooks": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"/cloud/how-tos/webhooks": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"/cloud/concepts/cron_jobs": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"/cloud/how-tos/cron_jobs": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"/how-tos/http/custom_lifespan": "https://docs.langchain.com/langsmith/custom-lifespan",
|
||||
"/how-tos/http/custom_middleware": "https://docs.langchain.com/langsmith/custom-middleware",
|
||||
"/how-tos/http/custom_routes": "https://docs.langchain.com/langsmith/custom-routes",
|
||||
"/cloud/concepts/data_storage_and_privacy": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
||||
"/cloud/deployment/semantic_search": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"/how-tos/ttl/configure_ttl": "https://docs.langchain.com/langsmith/configure-ttl",
|
||||
"/concepts/deployment_options": "https://docs.langchain.com/langsmith/deployments",
|
||||
"/cloud/quick_start": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
||||
"/cloud/deployment/setup": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
||||
"/cloud/deployment/setup_pyproject": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
"/cloud/deployment/setup_javascript": "https://docs.langchain.com/langsmith/setup-javascript",
|
||||
"/cloud/deployment/custom_docker": "https://docs.langchain.com/langsmith/custom-docker",
|
||||
"/cloud/deployment/graph_rebuild": "https://docs.langchain.com/langsmith/graph-rebuild",
|
||||
"/concepts/langgraph_cloud": "https://docs.langchain.com/langsmith/cloud",
|
||||
"/concepts/langgraph_self_hosted_data_plane": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/concepts/langgraph_self_hosted_control_plane": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/concepts/langgraph_standalone_container": "https://docs.langchain.com/langsmith/docker",
|
||||
"/cloud/deployment/cloud": "https://docs.langchain.com/langsmith/cloud",
|
||||
"/cloud/deployment/self_hosted_data_plane": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/cloud/deployment/self_hosted_control_plane": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"/cloud/deployment/standalone_container": "https://docs.langchain.com/langsmith/docker",
|
||||
"/concepts/server-mcp": "https://docs.langchain.com/langsmith/server-mcp",
|
||||
"/cloud/how-tos/human_in_the_loop_time_travel": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
||||
"/cloud/how-tos/add-human-in-the-loop": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"/cloud/deployment/egress": "https://docs.langchain.com/langsmith/env-var",
|
||||
"/cloud/how-tos/streaming": "https://docs.langchain.com/langsmith/streaming",
|
||||
"/cloud/reference/api/api_ref": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"/cloud/reference/langgraph_server_changelog": "https://docs.langchain.com/langsmith/agent-server-changelog",
|
||||
"/cloud/reference/api/api_ref_control_plane": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
||||
"/cloud/reference/cli": "https://docs.langchain.com/langsmith/cli",
|
||||
"/cloud/reference/env_var": "https://docs.langchain.com/langsmith/env-var",
|
||||
"/troubleshooting/studio": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
||||
"/index": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/agents/agents": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"/concepts/why-langgraph": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/get-started/1-build-basic-chatbot": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/get-started/2-add-tools": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/get-started/3-add-memory": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/get-started/4-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/get-started/5-customize-state": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/get-started/6-time-travel": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/tutorials/langsmith/local-server": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"/tutorials/workflows": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/tutorials/plan-and-execute/plan-and-execute": "https://docs.langchain.com/oss/python/langchain/middleware/built-in#to-do-list",
|
||||
"/tutorials/langgraph-platform/local-server/local-server": "https://docs.langchain.com/langsmith/local-server",
|
||||
"/concepts/agentic_concepts": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/agents/overview": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"/agents/run_agents": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"/concepts/low_level": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/graph-api": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/react-agent-from-scratch": "https://docs.langchain.com/oss/python/langchain/quickstart",
|
||||
"/concepts/functional_api": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"/how-tos/use-functional-api": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"/concepts/pregel": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
||||
"/concepts/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/concepts/persistence": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"/concepts/durable_execution": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
||||
"/concepts/memory": "https://docs.langchain.com/oss/python/langgraph/memory",
|
||||
"/how-tos/memory/add-memory": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/agents/context": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/agents/models": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/concepts/tools": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/how-tos/tool-calling": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/concepts/human_in_the_loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/how-tos/human_in_the_loop/add-human-in-the-loop": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"/concepts/time-travel": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"/how-tos/human_in_the_loop/time-travel": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"/concepts/subgraphs": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"/how-tos/subgraph": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"/concepts/multi_agent": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/agents/multi-agent": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"/how-tos/multi_agent": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/concepts/mcp": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/agents/mcp": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/concepts/tracing": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"/how-tos/enable-tracing": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"/agents/evals": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/concepts/template_applications": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/rag/langgraph_agentic_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/multi_agent/agent_supervisor": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/tutorials/sql/sql-agent": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
||||
"/agents/ui": "https://docs.langchain.com/oss/python/langgraph/ui",
|
||||
"/how-tos/run-id-langsmith": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"/troubleshooting/errors/index": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"/troubleshooting/errors/INVALID_CHAT_HISTORY": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"/troubleshooting/errors/INVALID_LICENSE": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"/adopters": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"/concepts/faq": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/agents/prebuilt": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"/reference/index": "https://reference.langchain.com/python/langgraph/",
|
||||
"/reference/graphs": "https://reference.langchain.com/python/langgraph/graphs/",
|
||||
"/reference/func": "https://reference.langchain.com/python/langgraph/func/",
|
||||
"/reference/pregel": "https://reference.langchain.com/python/langgraph/pregel/",
|
||||
"/reference/checkpoints": "https://reference.langchain.com/python/langgraph/checkpoints/",
|
||||
"/reference/store": "https://reference.langchain.com/python/langgraph/store/",
|
||||
"/reference/cache": "https://reference.langchain.com/python/langgraph/cache/",
|
||||
"/reference/types": "https://reference.langchain.com/python/langgraph/types/",
|
||||
"/reference/runtime": "https://reference.langchain.com/python/langgraph/runtime/",
|
||||
"/reference/config": "https://reference.langchain.com/python/langgraph/config/",
|
||||
"/reference/errors": "https://reference.langchain.com/python/langgraph/errors/",
|
||||
"/reference/constants": "https://reference.langchain.com/python/langgraph/constants/",
|
||||
"/reference/channels": "https://reference.langchain.com/python/langgraph/channels/",
|
||||
"/reference/agents": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"/reference/supervisor": "https://reference.langchain.com/python/langgraph/supervisor/",
|
||||
"/reference/swarm": "https://reference.langchain.com/python/langgraph/swarm/",
|
||||
"/reference/mcp": "https://reference.langchain.com/python/langgraph/mcp/",
|
||||
"/cloud/reference/sdk/python_sdk_ref": "https://reference.langchain.com/python/langsmith/deployment/sdk/",
|
||||
"/reference/remote_graph": "https://reference.langchain.com/python/langsmith/deployment/remote_graph/",
|
||||
"/additional-resources/index": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"/cloud/reference/sdk/js_ts_sdk_ref": "https://reference.langchain.com/javascript/modules/langsmith.html",
|
||||
"/snippets/chat_model_tabs": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"/troubleshooting/errors/GRAPH_RECURSION_LIMIT": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
||||
"/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
||||
"/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
||||
"/troubleshooting/errors/MULTIPLE_SUBGRAPHS": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
||||
"/tutorials/rag/langgraph_self_rag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/additional-resources": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/examples": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/guides": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/how-tos/autogen-integration-functional": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/how-tos/cross-thread-persistence-functional": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
|
||||
"/how-tos/disable-streaming": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"/how-tos/memory/semantic-search": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/multi-agent-multi-turn-convo-functional": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/multi-agent-network-functional": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"/how-tos/persistence-functional": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"/how-tos/react-agent-from-scratch-functional": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"/reference": "https://reference.langchain.com/python/langgraph/",
|
||||
"/troubleshooting/errors": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/chatbots/information-gather-prompting": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/extraction/retries": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/langgraph-platform/local-server": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"/tutorials/lats/lats": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/llm-compiler/LLMCompiler": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/rag/langgraph_adaptive_rag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/rag/langgraph_crag": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/rag/langgraph_crag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/rag/langgraph_self_rag_local": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"/tutorials/reflection/reflection": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/reflexion/reflexion": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/rewoo/rewoo": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/self-discover/self-discover": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/tnt-llm/tnt-llm": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/tot/tot": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/usaco/usaco": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"/tutorials/web-navigation/web_voyager": "https://docs.langchain.com/oss/python/langgraph/overview"
|
||||
}
|
||||
@@ -62,3 +62,4 @@ target-version = "py310"
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple/"
|
||||
publish-url = "https://test.pypi.org/legacy/"
|
||||
explicit = true
|
||||
|
||||
Generated
+37
-37
@@ -746,27 +746,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -834,26 +834,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.17"
|
||||
version = "0.0.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test test_watch lint format
|
||||
.PHONY: test test_watch lint type format
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -61,6 +61,9 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -13,7 +13,7 @@ license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
dependencies = [
|
||||
"langgraph-checkpoint>=2.1.2,<5.0.0",
|
||||
"orjson>=3.10.1",
|
||||
"orjson>=3.11.5",
|
||||
"psycopg>=3.2.0",
|
||||
"psycopg-pool>=3.2.0",
|
||||
]
|
||||
|
||||
Generated
+81
-80
@@ -259,7 +259,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "../checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -280,6 +280,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -346,7 +347,7 @@ test = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "psycopg", specifier = ">=3.2.0" },
|
||||
{ name = "psycopg-pool", specifier = ">=3.2.0" },
|
||||
]
|
||||
@@ -691,15 +692,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.2"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -709,64 +710,64 @@ binary = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.3.2"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/d8/a763308a41e2ecfb6256ba0877d340c2f2b124c8b2746401863d96fa2c7a/psycopg_binary-3.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c", size = 4609758, upload-time = "2026-02-18T16:46:33.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a9/f8a683e85400c1208685e7c895abc049dc13aa0b6ea989e6adf0a3681fe0/psycopg_binary-3.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84", size = 4676740, upload-time = "2026-02-18T16:46:42.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/7d/03512c4aaac8a58fc3b1221f38293aa517a1950d10ef8646c72c49addc7d/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b", size = 5496335, upload-time = "2026-02-18T16:46:51.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/bc/23319b4b1c2c0b810d225e1b6f16efbb16150074fc0ea96bfcabdf59ee09/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51", size = 5172032, upload-time = "2026-02-18T16:47:00.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c8/6d61dc0a56654c558a37b2d9b2094e470aa12621305cc7935fd769122e32/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7", size = 6763107, upload-time = "2026-02-18T16:47:11.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/b5/e2a3c90aa1059f5b5f593379caad7be3cc3c2ce1ddfc7730e39854e174fe/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e", size = 5006494, upload-time = "2026-02-18T16:47:17.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/3e/bf126e0a1f864e191b7f3eeea667ee2ce13d582b036255fb8b12946d1f7a/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1", size = 4533850, upload-time = "2026-02-18T16:47:21.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d8/bb5e8d395deb945629aa0c65d12ab90ec3bfcbdf56be89e2a84d001864c9/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e", size = 4223316, upload-time = "2026-02-18T16:47:25.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/70/33eef61b0f0fd41ebf93b9699f44067313a45016827f67b3c8cc41f0a7ab/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25", size = 3954515, upload-time = "2026-02-18T16:47:30.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/db/27c2b3b9698e713e83e11e8540daa27516f9e90390ec21a41091cb15fcaf/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f", size = 4260274, upload-time = "2026-02-18T16:47:36.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/3b/71e5d603059bf5474215f573a3e2d357a4e95672b26e04d41674400d4862/psycopg_binary-3.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0", size = 3557375, upload-time = "2026-02-18T16:47:42.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1073,27 +1074,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test test_watch lint format
|
||||
.PHONY: test test_watch lint type format
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -32,6 +32,9 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
Generated
+21
-20
@@ -268,7 +268,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "../checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -289,6 +289,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -997,27 +998,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test test_watch lint format
|
||||
.PHONY: test test_watch lint type format
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -32,6 +32,9 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
uv run ruff check --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
import copy
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, Sequence
|
||||
from typing import ( # noqa: UP035
|
||||
Any,
|
||||
Generic,
|
||||
@@ -14,6 +16,7 @@ from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base.id import uuid6
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import (
|
||||
ERROR,
|
||||
@@ -25,6 +28,7 @@ from langgraph.checkpoint.serde.types import (
|
||||
|
||||
V = TypeVar("V", int, float, str)
|
||||
PendingWrite = tuple[str, str, Any]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Marked as total=False to allow for future expansion.
|
||||
@@ -474,6 +478,37 @@ class BaseCheckpointSaver(Generic[V]):
|
||||
else:
|
||||
return current + 1
|
||||
|
||||
def with_allowlist(
|
||||
self, extra_allowlist: Collection[tuple[str, ...]]
|
||||
) -> BaseCheckpointSaver[V]:
|
||||
"""Return a shallow clone with a derived msgpack allowlist."""
|
||||
serde = _with_msgpack_allowlist(self.serde, extra_allowlist)
|
||||
if serde is self.serde:
|
||||
return self
|
||||
clone = copy.copy(self)
|
||||
clone.serde = maybe_add_typed_methods(serde)
|
||||
return clone
|
||||
|
||||
|
||||
def _with_msgpack_allowlist(
|
||||
serde: SerializerProtocol, extra_allowlist: Collection[tuple[str, ...]]
|
||||
) -> SerializerProtocol:
|
||||
if isinstance(serde, JsonPlusSerializer):
|
||||
return serde.with_msgpack_allowlist(extra_allowlist)
|
||||
if isinstance(serde, EncryptedSerializer):
|
||||
inner = serde.serde
|
||||
if isinstance(inner, JsonPlusSerializer):
|
||||
updated_inner = inner.with_msgpack_allowlist(extra_allowlist)
|
||||
if updated_inner is inner:
|
||||
return serde
|
||||
return EncryptedSerializer(serde.cipher, updated_inner)
|
||||
logger.warning(
|
||||
"Serializer %s does not support msgpack allowlist. "
|
||||
"Strict msgpack deserialization will not be enforced.",
|
||||
type(serde).__name__,
|
||||
)
|
||||
return serde
|
||||
|
||||
|
||||
class EmptyChannelError(Exception):
|
||||
"""Raised when attempting to get the value of a channel that hasn't been updated
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import cast
|
||||
|
||||
STRICT_MSGPACK_ENABLED = os.getenv("LANGGRAPH_STRICT_MSGPACK", "false").lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
)
|
||||
|
||||
|
||||
_SENTINEL = cast(None, object())
|
||||
|
||||
SAFE_MSGPACK_TYPES: frozenset[tuple[str, ...]] = frozenset(
|
||||
{
|
||||
# datetime types
|
||||
("datetime", "datetime"),
|
||||
("datetime", "date"),
|
||||
("datetime", "time"),
|
||||
("datetime", "timedelta"),
|
||||
("datetime", "timezone"),
|
||||
# uuid
|
||||
("uuid", "UUID"),
|
||||
# numeric
|
||||
("decimal", "Decimal"),
|
||||
# collections
|
||||
("builtins", "set"),
|
||||
("builtins", "frozenset"),
|
||||
("collections", "deque"),
|
||||
# ip addresses
|
||||
("ipaddress", "IPv4Address"),
|
||||
("ipaddress", "IPv4Interface"),
|
||||
("ipaddress", "IPv4Network"),
|
||||
("ipaddress", "IPv6Address"),
|
||||
("ipaddress", "IPv6Interface"),
|
||||
("ipaddress", "IPv6Network"),
|
||||
# pathlib
|
||||
("pathlib", "Path"),
|
||||
("pathlib", "PosixPath"),
|
||||
("pathlib", "WindowsPath"),
|
||||
# pathlib in Python 3.13+
|
||||
("pathlib._local", "Path"),
|
||||
("pathlib._local", "PosixPath"),
|
||||
("pathlib._local", "WindowsPath"),
|
||||
# zoneinfo
|
||||
("zoneinfo", "ZoneInfo"),
|
||||
# regex
|
||||
("re", "compile"),
|
||||
# langchain-core messages (safe container types used by graph state)
|
||||
("langchain_core.messages.base", "BaseMessage"),
|
||||
("langchain_core.messages.base", "BaseMessageChunk"),
|
||||
("langchain_core.messages.human", "HumanMessage"),
|
||||
("langchain_core.messages.human", "HumanMessageChunk"),
|
||||
("langchain_core.messages.ai", "AIMessage"),
|
||||
("langchain_core.messages.ai", "AIMessageChunk"),
|
||||
("langchain_core.messages.system", "SystemMessage"),
|
||||
("langchain_core.messages.system", "SystemMessageChunk"),
|
||||
("langchain_core.messages.chat", "ChatMessage"),
|
||||
("langchain_core.messages.chat", "ChatMessageChunk"),
|
||||
("langchain_core.messages.tool", "ToolMessage"),
|
||||
("langchain_core.messages.tool", "ToolMessageChunk"),
|
||||
("langchain_core.messages.function", "FunctionMessage"),
|
||||
("langchain_core.messages.function", "FunctionMessageChunk"),
|
||||
("langchain_core.messages.modifier", "RemoveMessage"),
|
||||
# langchain-core document model
|
||||
("langchain_core.documents.base", "Document"),
|
||||
# langgraph
|
||||
("langgraph.types", "Send"),
|
||||
("langgraph.types", "Interrupt"),
|
||||
("langgraph.types", "Command"),
|
||||
("langgraph.types", "StateSnapshot"),
|
||||
("langgraph.types", "PregelTask"),
|
||||
("langgraph.types", "Overwrite"),
|
||||
("langgraph.store.base", "Item"),
|
||||
("langgraph.store.base", "GetOp"),
|
||||
}
|
||||
)
|
||||
|
||||
# Allowed (module, name, method) triples for EXT_METHOD_SINGLE_ARG.
|
||||
# Only these specific method invocations are permitted during deserialization.
|
||||
# This is separate from SAFE_MSGPACK_TYPES which only governs construction.
|
||||
SAFE_MSGPACK_METHODS: frozenset[tuple[str, str, str]] = frozenset(
|
||||
{
|
||||
("datetime", "datetime", "fromisoformat"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
AllowedMsgpackModules = Iterable[tuple[str, ...] | type]
|
||||
@@ -41,7 +41,7 @@ class EncryptedSerializer(SerializerProtocol):
|
||||
) -> "EncryptedSerializer":
|
||||
"""Create an `EncryptedSerializer` using AES encryption."""
|
||||
try:
|
||||
from Crypto.Cipher import AES # type: ignore
|
||||
from Crypto.Cipher import AES
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Pycryptodome is not installed. Please install it with `pip install pycryptodome`."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import decimal
|
||||
import importlib
|
||||
@@ -10,7 +11,7 @@ import pickle
|
||||
import re
|
||||
import sys
|
||||
from collections import deque
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from inspect import isclass
|
||||
@@ -22,17 +23,24 @@ from ipaddress import (
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from uuid import UUID
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import ormsgpack
|
||||
from langchain_core.load.load import Reviver
|
||||
|
||||
from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import SendProtocol
|
||||
from langgraph.store.base import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langgraph.checkpoint.serde._msgpack import (
|
||||
AllowedMsgpackModules,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import SendProtocol
|
||||
|
||||
LC_REVIVER = Reviver()
|
||||
EMPTY_BYTES = b""
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -53,21 +61,62 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
self,
|
||||
*,
|
||||
pickle_fallback: bool = False,
|
||||
allowed_json_modules: Sequence[tuple[str, ...]] | Literal[True] | None = None,
|
||||
allowed_json_modules: Iterable[tuple[str, ...]] | Literal[True] | None = None,
|
||||
allowed_msgpack_modules: (
|
||||
AllowedMsgpackModules | Literal[True] | None
|
||||
) = _lg_msgpack._SENTINEL,
|
||||
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
|
||||
) -> None:
|
||||
if allowed_msgpack_modules is _lg_msgpack._SENTINEL:
|
||||
if _lg_msgpack.STRICT_MSGPACK_ENABLED:
|
||||
allowed_msgpack_modules = None
|
||||
else:
|
||||
allowed_msgpack_modules = True
|
||||
self.pickle_fallback = pickle_fallback
|
||||
self._allowed_modules = (
|
||||
{mod_and_name for mod_and_name in allowed_json_modules}
|
||||
if allowed_json_modules and allowed_json_modules is not True
|
||||
else (allowed_json_modules if allowed_json_modules is True else None)
|
||||
self._allowed_json_modules: set[tuple[str, ...]] | Literal[True] | None = (
|
||||
_normalize_allowlist(allowed_json_modules)
|
||||
)
|
||||
self._allowed_msgpack_modules = _normalize_allowlist(allowed_msgpack_modules)
|
||||
|
||||
self._custom_unpack_ext_hook = __unpack_ext_hook__ is not None
|
||||
self._unpack_ext_hook = (
|
||||
__unpack_ext_hook__
|
||||
if __unpack_ext_hook__ is not None
|
||||
else _msgpack_ext_hook
|
||||
else _create_msgpack_ext_hook(self._allowed_msgpack_modules)
|
||||
)
|
||||
|
||||
def with_msgpack_allowlist(
|
||||
self, extra_allowlist: Iterable[tuple[str, ...] | type]
|
||||
) -> JsonPlusSerializer:
|
||||
"""Return a new serializer with a merged msgpack allowlist."""
|
||||
base_allowlist = self._allowed_msgpack_modules
|
||||
if base_allowlist is True or base_allowlist is False:
|
||||
return self
|
||||
elif base_allowlist:
|
||||
base_allowlist = set(base_allowlist)
|
||||
else:
|
||||
base_allowlist = set()
|
||||
extra = _normalize_module_keys(tuple(extra_allowlist))
|
||||
merged = base_allowlist | extra
|
||||
if merged == base_allowlist:
|
||||
return self
|
||||
allowed_msgpack_modules: AllowedMsgpackModules | Literal[True] | None
|
||||
if merged:
|
||||
allowed_msgpack_modules = tuple(merged)
|
||||
elif isinstance(self._allowed_msgpack_modules, set):
|
||||
allowed_msgpack_modules = tuple(self._allowed_msgpack_modules)
|
||||
else:
|
||||
allowed_msgpack_modules = self._allowed_msgpack_modules
|
||||
|
||||
clone = copy.copy(self)
|
||||
clone._allowed_json_modules = _normalize_allowlist(self._allowed_json_modules)
|
||||
clone._allowed_msgpack_modules = _normalize_allowlist(allowed_msgpack_modules)
|
||||
if not clone._custom_unpack_ext_hook:
|
||||
clone._unpack_ext_hook = _create_msgpack_ext_hook(
|
||||
clone._allowed_msgpack_modules
|
||||
)
|
||||
return clone
|
||||
|
||||
def _encode_constructor_args(
|
||||
self,
|
||||
constructor: Callable | type[Any],
|
||||
@@ -90,7 +139,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return out
|
||||
|
||||
def _reviver(self, value: dict[str, Any]) -> Any:
|
||||
if self._allowed_modules and (
|
||||
if self._allowed_json_modules and (
|
||||
value.get("lc", None) == 2
|
||||
and value.get("type", None) == "constructor"
|
||||
and value.get("id", None) is not None
|
||||
@@ -107,7 +156,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return LC_REVIVER(value)
|
||||
|
||||
def _revive_lc2(self, value: dict[str, Any]) -> Any:
|
||||
self._check_allowed_modules(value)
|
||||
self._check_allowed_json_modules(value)
|
||||
|
||||
[*module, name] = value["id"]
|
||||
try:
|
||||
@@ -139,7 +188,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _check_allowed_modules(self, value: dict[str, Any]) -> None:
|
||||
def _check_allowed_json_modules(self, value: dict[str, Any]) -> None:
|
||||
needed = tuple(value["id"])
|
||||
method = value.get("method")
|
||||
if isinstance(method, list):
|
||||
@@ -150,7 +199,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
method_display = "<init>"
|
||||
|
||||
dotted = ".".join(needed)
|
||||
if not self._allowed_modules:
|
||||
if not self._allowed_json_modules:
|
||||
raise InvalidModuleError(
|
||||
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
|
||||
"No allowed_json_modules configured.\n\n"
|
||||
@@ -161,9 +210,9 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
"or plain-JSON representations revived without import-time side effects."
|
||||
)
|
||||
|
||||
if self._allowed_modules is True:
|
||||
if self._allowed_json_modules is True:
|
||||
return
|
||||
if needed in self._allowed_modules:
|
||||
if needed in self._allowed_json_modules:
|
||||
return
|
||||
|
||||
raise InvalidModuleError(
|
||||
@@ -448,92 +497,174 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
|
||||
raise TypeError(f"Object of type {obj.__class__.__name__} is not serializable")
|
||||
|
||||
|
||||
def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, arg
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, arg, method
|
||||
return getattr(getattr(importlib.import_module(tup[0]), tup[1]), tup[3])(
|
||||
tup[2]
|
||||
)
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, kwargs
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
return cls(**tup[2])
|
||||
except Exception:
|
||||
return cls.construct(**tup[2])
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, kwargs, method
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
return cls(**tup[2])
|
||||
except Exception:
|
||||
return cls.model_construct(**tup[2])
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return
|
||||
elif code == EXT_NUMPY_ARRAY:
|
||||
try:
|
||||
import numpy as _np
|
||||
def _create_msgpack_ext_hook(
|
||||
allowed_modules: set[tuple[str, ...]] | Literal[True] | None,
|
||||
) -> Callable[[int, bytes], Any]:
|
||||
"""Create msgpack ext hook with allowlist.
|
||||
|
||||
dtype_str, shape, order, buf = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
Args:
|
||||
allowed_modules: Set of (module, name) tuples that are allowed to be
|
||||
deserialized, or True to allow all with warnings for unregistered types, or None to only allow safe types.
|
||||
|
||||
Returns:
|
||||
An ext_hook function for use with ormsgpack.unpackb.
|
||||
"""
|
||||
|
||||
def _check_allowed(module: str, name: str) -> bool:
|
||||
"""Check if type is allowed. Returns True if allowed, False if blocked."""
|
||||
key = (module, name)
|
||||
|
||||
if key in _lg_msgpack.SAFE_MSGPACK_TYPES:
|
||||
return True
|
||||
|
||||
if allowed_modules is True:
|
||||
# default is to warn but allow unregistered types
|
||||
logger.warning(
|
||||
"Deserializing unregistered type %s.%s from checkpoint. "
|
||||
"This will be blocked in a future version. "
|
||||
"Add to allowed_msgpack_modules to silence: [(%r, %r)]",
|
||||
module,
|
||||
name,
|
||||
module,
|
||||
name,
|
||||
)
|
||||
arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
|
||||
return arr.reshape(shape, order=order)
|
||||
except Exception:
|
||||
return
|
||||
return True
|
||||
if allowed_modules is not None:
|
||||
if key in allowed_modules:
|
||||
return True
|
||||
# strict mode blocks unregistered types
|
||||
logger.warning(
|
||||
"Blocked deserialization of %s.%s - not in allowed_msgpack_modules. "
|
||||
"Add to allowed_msgpack_modules to allow: [(%r, %r)]",
|
||||
module,
|
||||
name,
|
||||
module,
|
||||
name,
|
||||
)
|
||||
return False
|
||||
|
||||
def _check_allowed_method(module: str, name: str, method: str) -> bool:
|
||||
"""Check if a method invocation is allowed."""
|
||||
key = (module, name, method)
|
||||
if key in _lg_msgpack.SAFE_MSGPACK_METHODS:
|
||||
return True
|
||||
logger.warning(
|
||||
"Blocked deserialization of method call %s.%s.%s - "
|
||||
"not in allowed methods set.",
|
||||
module,
|
||||
name,
|
||||
method,
|
||||
)
|
||||
return False
|
||||
|
||||
def ext_hook(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed(tup[0], tup[1]):
|
||||
# We default to returning the raw data. If the user
|
||||
# is using this in the context of a pydantic state, etc., then
|
||||
# it would be validated upon construction.
|
||||
return tup[2]
|
||||
# module, name, arg
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
|
||||
except Exception:
|
||||
return None
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed(tup[0], tup[1]):
|
||||
return tup[2]
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
|
||||
except Exception:
|
||||
return None
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed(tup[0], tup[1]):
|
||||
return tup[2]
|
||||
# module, name, kwargs
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
|
||||
except Exception:
|
||||
return None
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed_method(tup[0], tup[1], tup[3]):
|
||||
return tup[2]
|
||||
# module, name, arg, method
|
||||
return getattr(
|
||||
getattr(importlib.import_module(tup[0]), tup[1]), tup[3]
|
||||
)(tup[2])
|
||||
except Exception:
|
||||
return None
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed(tup[0], tup[1]):
|
||||
return tup[2]
|
||||
# module, name, kwargs
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
return cls(**tup[2])
|
||||
except Exception:
|
||||
return cls.construct(**tup[2])
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return None
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
if not _check_allowed(tup[0], tup[1]):
|
||||
return tup[2]
|
||||
# module, name, kwargs, method
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
try:
|
||||
return cls(**tup[2])
|
||||
except Exception:
|
||||
return cls.model_construct(**tup[2])
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return None
|
||||
elif code == EXT_NUMPY_ARRAY:
|
||||
try:
|
||||
import numpy as _np
|
||||
|
||||
dtype_str, shape, order, buf = ormsgpack.unpackb(
|
||||
data, ext_hook=ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
|
||||
return arr.reshape(shape, order=order)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
return ext_hook
|
||||
|
||||
|
||||
# Aliasing in case anyone imported it directly
|
||||
_msgpack_ext_hook = _create_msgpack_ext_hook(allowed_modules=None)
|
||||
|
||||
|
||||
def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
||||
@@ -648,3 +779,26 @@ _option = (
|
||||
|
||||
def _msgpack_enc(data: Any) -> bytes:
|
||||
return ormsgpack.packb(data, default=_msgpack_default, option=_option)
|
||||
|
||||
|
||||
def _normalize_allowlist(
|
||||
allowlist: AllowedMsgpackModules | Literal[True] | None,
|
||||
) -> set[tuple[str, ...]] | Literal[True] | None:
|
||||
if allowlist is True:
|
||||
return allowlist
|
||||
elif allowlist:
|
||||
return _normalize_module_keys(allowlist)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_module_keys(
|
||||
modules: AllowedMsgpackModules,
|
||||
) -> set[tuple[str, ...]]:
|
||||
normalized: set[tuple[str, ...]] = set()
|
||||
for module in modules:
|
||||
if isclass(module):
|
||||
normalized.add((module.__module__, module.__name__))
|
||||
else:
|
||||
normalized.add(cast(tuple[str, ...], module))
|
||||
return normalized
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -42,6 +42,7 @@ lint = [
|
||||
dev = [
|
||||
{include-group = "test"},
|
||||
{include-group = "lint"},
|
||||
"pycryptodome>=3.23.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
"""Tests for EncryptedSerializer with msgpack allowlist functionality.
|
||||
|
||||
These tests mirror the msgpack allowlist tests in test_jsonplus.py but run them
|
||||
through the EncryptedSerializer to ensure the allowlist behavior is preserved
|
||||
when encryption is enabled.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import uuid
|
||||
from collections import deque
|
||||
from datetime import date, datetime, time, timezone
|
||||
from decimal import Decimal
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Literal, cast
|
||||
|
||||
import ormsgpack
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, _with_msgpack_allowlist
|
||||
from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
|
||||
from langgraph.checkpoint.serde.base import CipherProtocol
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.serde.jsonplus import (
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
JsonPlusSerializer,
|
||||
_msgpack_enc,
|
||||
)
|
||||
|
||||
|
||||
class InnerPydantic(BaseModel):
|
||||
hello: str
|
||||
|
||||
|
||||
class MyPydantic(BaseModel):
|
||||
foo: str
|
||||
bar: int
|
||||
inner: InnerPydantic
|
||||
|
||||
|
||||
class AnotherPydantic(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
class _PassthroughCipher(CipherProtocol):
|
||||
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
|
||||
return "passthrough", plaintext
|
||||
|
||||
def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
|
||||
assert ciphername == "passthrough"
|
||||
return ciphertext
|
||||
|
||||
|
||||
def _make_encrypted_serde(
|
||||
allowed_msgpack_modules: (
|
||||
_lg_msgpack.AllowedMsgpackModules | Literal[True] | None | object
|
||||
) = _lg_msgpack._SENTINEL,
|
||||
) -> EncryptedSerializer:
|
||||
"""Create an EncryptedSerializer with AES encryption for testing."""
|
||||
inner = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=cast(
|
||||
_lg_msgpack.AllowedMsgpackModules | Literal[True] | None,
|
||||
allowed_msgpack_modules,
|
||||
)
|
||||
)
|
||||
return EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
|
||||
def test_msgpack_method_pathlib_blocked_encrypted_strict(
|
||||
tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
target = tmp_path / "secret.txt"
|
||||
target.write_text("secret")
|
||||
payload = ormsgpack.packb(
|
||||
ormsgpack.Ext(
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
_msgpack_enc(("pathlib", "Path", target, "read_text")),
|
||||
),
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
serde = EncryptedSerializer(
|
||||
_PassthroughCipher(),
|
||||
JsonPlusSerializer(allowed_msgpack_modules=None),
|
||||
)
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(("msgpack+passthrough", payload))
|
||||
|
||||
assert result == target
|
||||
assert "blocked deserialization of method call pathlib.path.read_text" in (
|
||||
caplog.text.lower()
|
||||
)
|
||||
|
||||
|
||||
class TestEncryptedSerializerMsgpackAllowlist:
|
||||
"""Test msgpack allowlist behavior through EncryptedSerializer."""
|
||||
|
||||
def test_safe_types_no_warning(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test safe types deserialize without warnings through encryption."""
|
||||
serde = _make_encrypted_serde()
|
||||
|
||||
safe_objects = [
|
||||
datetime.now(),
|
||||
date.today(),
|
||||
time(12, 30),
|
||||
timezone.utc,
|
||||
uuid.uuid4(),
|
||||
Decimal("123.45"),
|
||||
{1, 2, 3},
|
||||
frozenset([1, 2, 3]),
|
||||
deque([1, 2, 3]),
|
||||
IPv4Address("192.168.1.1"),
|
||||
pathlib.Path("/tmp/test"),
|
||||
]
|
||||
|
||||
for obj in safe_objects:
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
# Verify encryption is happening
|
||||
assert "+aes" in dumped[0], f"Expected encryption for {type(obj)}"
|
||||
result = serde.loads_typed(dumped)
|
||||
assert "unregistered type" not in caplog.text.lower(), (
|
||||
f"Unexpected warning for {type(obj)}"
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
def test_pydantic_warns_by_default(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Pydantic models not in allowlist should log warning but still deserialize."""
|
||||
current = _lg_msgpack.STRICT_MSGPACK_ENABLED
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = False
|
||||
serde = _make_encrypted_serde()
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0]
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" in caplog.text.lower()
|
||||
assert "allowed_msgpack_modules" in caplog.text
|
||||
assert result == obj
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = current
|
||||
|
||||
def test_strict_mode_blocks_unregistered(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Strict mode should block unregistered types through encryption."""
|
||||
serde = _make_encrypted_serde(allowed_msgpack_modules=None)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0]
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
expected = obj.model_dump()
|
||||
assert result == expected
|
||||
|
||||
def test_allowlist_silences_warning(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Types in allowed_msgpack_modules should deserialize without warnings."""
|
||||
serde = _make_encrypted_serde(
|
||||
allowed_msgpack_modules=[
|
||||
("tests.test_encrypted", "MyPydantic"),
|
||||
("tests.test_encrypted", "InnerPydantic"),
|
||||
]
|
||||
)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0]
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" not in caplog.text.lower()
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
def test_allowlist_blocks_non_listed(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Allowlists should block unregistered types even through encryption."""
|
||||
serde = _make_encrypted_serde(
|
||||
allowed_msgpack_modules=[("tests.test_encrypted", "MyPydantic")]
|
||||
)
|
||||
|
||||
obj = AnotherPydantic(foo="nope")
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0]
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
expected = obj.model_dump()
|
||||
assert result == expected
|
||||
|
||||
def test_safe_types_value_equality(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Verify safe types are correctly restored with proper values through encryption."""
|
||||
serde = _make_encrypted_serde(allowed_msgpack_modules=None)
|
||||
|
||||
test_cases = [
|
||||
datetime(2024, 1, 15, 12, 30, 45, 123456),
|
||||
date(2024, 6, 15),
|
||||
time(14, 30, 0),
|
||||
uuid.UUID("12345678-1234-5678-1234-567812345678"),
|
||||
Decimal("123.456789"),
|
||||
{1, 2, 3, 4, 5},
|
||||
frozenset(["a", "b", "c"]),
|
||||
deque([1, 2, 3]),
|
||||
IPv4Address("10.0.0.1"),
|
||||
pathlib.Path("/some/test/path"),
|
||||
re.compile(r"\d+", re.MULTILINE),
|
||||
]
|
||||
|
||||
for obj in test_cases:
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0], f"Expected encryption for {type(obj)}"
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" not in caplog.text.lower(), f"Blocked for {type(obj)}"
|
||||
if isinstance(obj, re.Pattern):
|
||||
assert result.pattern == obj.pattern
|
||||
assert result.flags == obj.flags
|
||||
else:
|
||||
assert result == obj, (
|
||||
f"Value mismatch for {type(obj)}: {result} != {obj}"
|
||||
)
|
||||
|
||||
def test_regex_safe_type(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""re.compile patterns should deserialize without warnings as a safe type."""
|
||||
serde = _make_encrypted_serde(allowed_msgpack_modules=None)
|
||||
pattern = re.compile(r"foo.*bar", re.IGNORECASE | re.DOTALL)
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(pattern)
|
||||
assert "+aes" in dumped[0]
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert "unregistered" not in caplog.text.lower()
|
||||
assert result.pattern == pattern.pattern
|
||||
assert result.flags == pattern.flags
|
||||
|
||||
|
||||
class TestWithMsgpackAllowlistEncrypted:
|
||||
"""Test _with_msgpack_allowlist function with EncryptedSerializer."""
|
||||
|
||||
def test_propagates_allowlist_to_inner_serde(self) -> None:
|
||||
"""_with_msgpack_allowlist should propagate allowlist to inner JsonPlusSerializer."""
|
||||
inner = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
extra = [("my.module", "MyClass")]
|
||||
result = _with_msgpack_allowlist(encrypted, extra)
|
||||
|
||||
# Should return a new EncryptedSerializer
|
||||
assert isinstance(result, EncryptedSerializer)
|
||||
assert result is not encrypted
|
||||
# Inner serde should have the allowlist
|
||||
assert isinstance(result.serde, JsonPlusSerializer)
|
||||
assert isinstance(result.serde._allowed_msgpack_modules, set)
|
||||
assert ("my.module", "MyClass") in result.serde._allowed_msgpack_modules
|
||||
|
||||
def test_preserves_cipher(self) -> None:
|
||||
"""_with_msgpack_allowlist should preserve the cipher from the original."""
|
||||
inner = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
result = _with_msgpack_allowlist(encrypted, [("my.module", "MyClass")])
|
||||
|
||||
assert isinstance(result, EncryptedSerializer)
|
||||
# Should use the same cipher
|
||||
assert result.cipher is encrypted.cipher
|
||||
|
||||
def test_returns_same_if_not_jsonplus_inner(self) -> None:
|
||||
"""_with_msgpack_allowlist should return same serde if inner is not JsonPlusSerializer."""
|
||||
|
||||
class DummyInnerSerde:
|
||||
def dumps_typed(self, obj: object) -> tuple[str, bytes]:
|
||||
return ("dummy", b"")
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> None:
|
||||
return None
|
||||
|
||||
from langgraph.checkpoint.serde.base import CipherProtocol
|
||||
|
||||
class DummyCipher(CipherProtocol):
|
||||
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
|
||||
return "dummy", plaintext
|
||||
|
||||
def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
|
||||
return ciphertext
|
||||
|
||||
encrypted = EncryptedSerializer(DummyCipher(), DummyInnerSerde())
|
||||
result = _with_msgpack_allowlist(encrypted, [("my.module", "MyClass")])
|
||||
|
||||
assert result is encrypted
|
||||
|
||||
def test_warns_if_allowlist_unsupported(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
class DummySerde:
|
||||
def dumps_typed(self, obj: object) -> tuple[str, bytes]:
|
||||
return ("dummy", b"")
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> object:
|
||||
return data
|
||||
|
||||
serde = DummySerde()
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.base")
|
||||
caplog.clear()
|
||||
|
||||
result = _with_msgpack_allowlist(serde, [("my.module", "MyClass")])
|
||||
|
||||
assert result is serde
|
||||
assert "does not support msgpack allowlist" in caplog.text.lower()
|
||||
|
||||
def test_noop_allowlist_returns_same_encrypted_instance(self) -> None:
|
||||
inner = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
result = _with_msgpack_allowlist(encrypted, ())
|
||||
|
||||
assert result is encrypted
|
||||
|
||||
def test_functional_roundtrip_with_allowlist(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""End-to-end test: allowlist applied via _with_msgpack_allowlist works."""
|
||||
inner = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
# Apply allowlist for MyPydantic
|
||||
updated = _with_msgpack_allowlist(
|
||||
encrypted,
|
||||
[
|
||||
("tests.test_encrypted", "MyPydantic"),
|
||||
("tests.test_encrypted", "InnerPydantic"),
|
||||
],
|
||||
)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = updated.dumps_typed(obj)
|
||||
assert "+aes" in dumped[0]
|
||||
result = updated.loads_typed(dumped)
|
||||
|
||||
# Should deserialize without blocking
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
def test_original_still_blocks_after_with_allowlist(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Original serde should still block after _with_msgpack_allowlist creates a new one."""
|
||||
inner = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=inner, key=b"1234567890123456"
|
||||
)
|
||||
|
||||
# Apply allowlist - this should create a NEW serde
|
||||
_with_msgpack_allowlist(
|
||||
encrypted,
|
||||
[("tests.test_encrypted", "MyPydantic")],
|
||||
)
|
||||
|
||||
# Original should still block
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = encrypted.dumps_typed(obj)
|
||||
result = encrypted.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
assert result == obj.model_dump()
|
||||
|
||||
|
||||
class TestEncryptedSerializerUnencryptedFallback:
|
||||
"""Test that EncryptedSerializer handles unencrypted data correctly."""
|
||||
|
||||
def test_loads_unencrypted_data(self) -> None:
|
||||
"""EncryptedSerializer should handle unencrypted data for backwards compat."""
|
||||
plain = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
encrypted = _make_encrypted_serde(allowed_msgpack_modules=None)
|
||||
|
||||
obj = {"key": "value", "number": 42}
|
||||
|
||||
# Serialize with plain serde
|
||||
dumped = plain.dumps_typed(obj)
|
||||
assert "+aes" not in dumped[0]
|
||||
|
||||
# Should still deserialize with encrypted serde
|
||||
result = encrypted.loads_typed(dumped)
|
||||
assert result == obj
|
||||
|
||||
|
||||
def test_with_allowlist_uses_copy_protocol() -> None:
|
||||
class CopyAwareSaver(BaseCheckpointSaver[str]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(serde=JsonPlusSerializer(allowed_msgpack_modules=None))
|
||||
self.copy_was_used = False
|
||||
|
||||
def __copy__(self) -> object:
|
||||
clone = object.__new__(self.__class__)
|
||||
clone.__dict__ = self.__dict__.copy()
|
||||
clone.copy_was_used = True
|
||||
return clone
|
||||
|
||||
saver = CopyAwareSaver()
|
||||
|
||||
updated = saver.with_allowlist([("tests.test_encrypted", "MyPydantic")])
|
||||
|
||||
assert isinstance(updated, CopyAwareSaver)
|
||||
assert updated is not saver
|
||||
assert updated.copy_was_used is True
|
||||
assert saver.copy_was_used is False
|
||||
@@ -1,5 +1,6 @@
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
@@ -13,15 +14,22 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import dataclasses_json
|
||||
import numpy as np
|
||||
import ormsgpack
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from langchain_core.documents.base import Document
|
||||
from langchain_core.messages import HumanMessage
|
||||
from pydantic import BaseModel, SecretStr
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import SecretStr as SecretStrV1
|
||||
|
||||
from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
|
||||
from langgraph.checkpoint.serde._msgpack import AllowedMsgpackModules
|
||||
from langgraph.checkpoint.serde.jsonplus import (
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
InvalidModuleError,
|
||||
JsonPlusSerializer,
|
||||
_msgpack_enc,
|
||||
_msgpack_ext_hook_to_json,
|
||||
)
|
||||
from langgraph.store.base import Item
|
||||
@@ -37,6 +45,10 @@ class MyPydantic(BaseModel):
|
||||
inner: InnerPydantic
|
||||
|
||||
|
||||
class AnotherPydantic(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
class InnerPydanticV1(BaseModelV1):
|
||||
hello: str
|
||||
|
||||
@@ -138,7 +150,27 @@ def test_serde_jsonplus() -> None:
|
||||
)
|
||||
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
|
||||
|
||||
serde = JsonPlusSerializer()
|
||||
allowed_msgpack_modules: AllowedMsgpackModules = [
|
||||
InnerDataclass,
|
||||
MyDataclass,
|
||||
MyDataclassWSlots,
|
||||
MyEnum,
|
||||
InnerPydantic,
|
||||
MyPydantic,
|
||||
# Testing that it supports both.
|
||||
(Person.__module__, Person.__name__),
|
||||
(SecretStr.__module__, SecretStr.__name__),
|
||||
]
|
||||
if sys.version_info < (3, 14):
|
||||
allowed_msgpack_modules.extend( # type: ignore
|
||||
[
|
||||
(InnerPydanticV1.__module__, InnerPydanticV1.__name__),
|
||||
(MyPydanticV1.__module__, MyPydanticV1.__name__),
|
||||
(SecretStrV1.__module__, SecretStrV1.__name__),
|
||||
]
|
||||
)
|
||||
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=allowed_msgpack_modules)
|
||||
|
||||
dumped = serde.dumps_typed(to_serialize)
|
||||
|
||||
@@ -512,5 +544,404 @@ def test_serde_jsonplus_pandas_series(series: pd.Series) -> None:
|
||||
|
||||
assert dumped[0] == "pickle"
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert result.equals(series)
|
||||
|
||||
|
||||
def test_msgpack_safe_types_no_warning(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test safe types deserialize without warnings."""
|
||||
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
safe_objects = [
|
||||
datetime.now(),
|
||||
date.today(),
|
||||
time(12, 30),
|
||||
timezone.utc,
|
||||
uuid.uuid4(),
|
||||
Decimal("123.45"),
|
||||
{1, 2, 3},
|
||||
frozenset([1, 2, 3]),
|
||||
deque([1, 2, 3]),
|
||||
IPv4Address("192.168.1.1"),
|
||||
pathlib.Path("/tmp/test"),
|
||||
]
|
||||
|
||||
for obj in safe_objects:
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
assert "unregistered type" not in caplog.text.lower(), (
|
||||
f"Unexpected warning for {type(obj)}"
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_msgpack_pydantic_warns_by_default(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Pydantic models not in allowlist should log warning but still deserialize."""
|
||||
current = _lg_msgpack.STRICT_MSGPACK_ENABLED
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = False
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" in caplog.text.lower()
|
||||
assert "allowed_msgpack_modules" in caplog.text
|
||||
assert result == obj
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = current
|
||||
|
||||
|
||||
def test_msgpack_env_strict_default(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Strict msgpack env should default to blocking unregistered types."""
|
||||
current = _lg_msgpack.STRICT_MSGPACK_ENABLED
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = True
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
assert result == obj.model_dump()
|
||||
_lg_msgpack.STRICT_MSGPACK_ENABLED = current
|
||||
|
||||
|
||||
def test_msgpack_allowlist_silences_warning(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Types in allowed_msgpack_modules should deserialize without warnings."""
|
||||
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[
|
||||
("tests.test_jsonplus", "MyPydantic"),
|
||||
("tests.test_jsonplus", "InnerPydantic"),
|
||||
]
|
||||
)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
|
||||
def test_msgpack_none_blocks_unregistered(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""allowed_msgpack_modules=None should block unregistered types."""
|
||||
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
expected = obj.model_dump()
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_msgpack_allowlist_blocks_non_listed(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Allowlists should block unregistered types even if msgpack is enabled."""
|
||||
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[("tests.test_jsonplus", "MyPydantic")]
|
||||
)
|
||||
|
||||
obj = AnotherPydantic(foo="nope")
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" in caplog.text.lower()
|
||||
expected = obj.model_dump()
|
||||
# It's not allowed, so we just leave it as a dict
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_msgpack_strict_allows_safe_types(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Safe types should still deserialize in strict mode without warnings."""
|
||||
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
safe = uuid.uuid4()
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(safe)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == safe
|
||||
|
||||
|
||||
def test_msgpack_strict_allows_core_langchain_messages(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
msg = HumanMessage(content="hello")
|
||||
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(serde.dumps_typed(msg))
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert "unregistered" not in caplog.text.lower()
|
||||
assert isinstance(result, HumanMessage)
|
||||
assert result == msg
|
||||
|
||||
|
||||
def test_msgpack_strict_allows_langchain_document(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
doc = Document(page_content="hello", metadata={"k": "v"})
|
||||
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(serde.dumps_typed(doc))
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert "unregistered" not in caplog.text.lower()
|
||||
assert isinstance(result, Document)
|
||||
assert result == doc
|
||||
|
||||
|
||||
def test_msgpack_regex_safe_type(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""re.compile patterns should deserialize without warnings as a safe type."""
|
||||
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
pattern = re.compile(r"foo.*bar", re.IGNORECASE | re.DOTALL)
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(pattern)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert "unregistered" not in caplog.text.lower()
|
||||
assert result.pattern == pattern.pattern
|
||||
assert result.flags == pattern.flags
|
||||
|
||||
|
||||
def test_msgpack_method_pathlib_blocked_in_strict(
|
||||
tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
target = tmp_path / "secret.txt"
|
||||
target.write_text("secret")
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
payload = ormsgpack.packb(
|
||||
ormsgpack.Ext(
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
_msgpack_enc(("pathlib", "Path", target, "read_text")),
|
||||
),
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(("msgpack", payload))
|
||||
|
||||
assert result == target
|
||||
assert "blocked deserialization of method call pathlib.path.read_text" in (
|
||||
caplog.text.lower()
|
||||
)
|
||||
|
||||
|
||||
def test_msgpack_method_pathlib_blocked_default_mode(
|
||||
tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
target = tmp_path / "secret.txt"
|
||||
target.write_text("secret")
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=True)
|
||||
payload = ormsgpack.packb(
|
||||
ormsgpack.Ext(
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
_msgpack_enc(("pathlib", "Path", target, "read_text")),
|
||||
),
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(("msgpack", payload))
|
||||
|
||||
assert result == target
|
||||
assert "blocked deserialization of method call pathlib.path.read_text" in (
|
||||
caplog.text.lower()
|
||||
)
|
||||
|
||||
|
||||
def test_msgpack_regex_still_works_strict(caplog: pytest.LogCaptureFixture) -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
pattern = re.compile(r"pattern", re.IGNORECASE | re.MULTILINE)
|
||||
|
||||
caplog.clear()
|
||||
result = serde.loads_typed(serde.dumps_typed(pattern))
|
||||
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result.pattern == pattern.pattern
|
||||
assert result.flags == pattern.flags
|
||||
|
||||
|
||||
def test_msgpack_path_constructor_still_works() -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
path_obj = pathlib.Path("/tmp/foo")
|
||||
|
||||
result = serde.loads_typed(serde.dumps_typed(path_obj))
|
||||
|
||||
assert result == path_obj
|
||||
|
||||
|
||||
def test_with_msgpack_allowlist_noop_returns_same_instance() -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
|
||||
result = serde.with_msgpack_allowlist(())
|
||||
|
||||
assert result is serde
|
||||
|
||||
|
||||
def test_with_msgpack_allowlist_supports_subclass_without_init_kwargs() -> None:
|
||||
class CustomSerializer(JsonPlusSerializer):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(allowed_msgpack_modules=None)
|
||||
|
||||
serde = CustomSerializer()
|
||||
result = serde.with_msgpack_allowlist([MyDataclass])
|
||||
|
||||
assert isinstance(result, CustomSerializer)
|
||||
assert result is not serde
|
||||
assert serde._allowed_msgpack_modules is None
|
||||
assert result._allowed_msgpack_modules == {
|
||||
(MyDataclass.__module__, MyDataclass.__name__)
|
||||
}
|
||||
|
||||
|
||||
def test_with_msgpack_allowlist_rebuilds_default_unpack_hook() -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
original_hook = serde._unpack_ext_hook
|
||||
|
||||
result = serde.with_msgpack_allowlist([MyDataclass])
|
||||
|
||||
assert result._unpack_ext_hook is not original_hook
|
||||
|
||||
|
||||
def test_with_msgpack_allowlist_preserves_custom_unpack_hook() -> None:
|
||||
def custom_hook(code: int, data: bytes) -> None:
|
||||
return None
|
||||
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=None, __unpack_ext_hook__=custom_hook
|
||||
)
|
||||
result = serde.with_msgpack_allowlist([MyDataclass])
|
||||
|
||||
assert result._unpack_ext_hook is custom_hook
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info >= (3, 14), reason="pydantic v1 not on 3.14+")
|
||||
def test_msgpack_pydantic_v1_allowlist(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Pydantic v1 models in allowlist should deserialize without warnings."""
|
||||
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[
|
||||
("tests.test_jsonplus", "MyPydanticV1"),
|
||||
("tests.test_jsonplus", "InnerPydanticV1"),
|
||||
]
|
||||
)
|
||||
|
||||
obj = MyPydanticV1(foo="test", bar=42, inner=InnerPydanticV1(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" not in caplog.text.lower()
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
|
||||
def test_msgpack_dataclass_allowlist(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Dataclasses in allowlist should deserialize without warnings."""
|
||||
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[
|
||||
("tests.test_jsonplus", "MyDataclass"),
|
||||
("tests.test_jsonplus", "InnerDataclass"),
|
||||
]
|
||||
)
|
||||
|
||||
obj = MyDataclass(foo="test", bar=42, inner=InnerDataclass(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "unregistered type" not in caplog.text.lower()
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
|
||||
def test_msgpack_safe_types_value_equality(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Verify safe types are correctly restored with proper values."""
|
||||
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
|
||||
test_cases = [
|
||||
datetime(2024, 1, 15, 12, 30, 45, 123456),
|
||||
date(2024, 6, 15),
|
||||
time(14, 30, 0),
|
||||
uuid.UUID("12345678-1234-5678-1234-567812345678"),
|
||||
Decimal("123.456789"),
|
||||
{1, 2, 3, 4, 5},
|
||||
frozenset(["a", "b", "c"]),
|
||||
deque([1, 2, 3]),
|
||||
IPv4Address("10.0.0.1"),
|
||||
pathlib.Path("/some/test/path"),
|
||||
re.compile(r"\d+", re.MULTILINE),
|
||||
]
|
||||
|
||||
for obj in test_cases:
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
assert "blocked" not in caplog.text.lower(), f"Blocked for {type(obj)}"
|
||||
# For regex patterns, compare pattern and flags
|
||||
if isinstance(obj, re.Pattern):
|
||||
assert result.pattern == obj.pattern
|
||||
assert result.flags == obj.flags
|
||||
else:
|
||||
assert result == obj, f"Value mismatch for {type(obj)}: {result} != {obj}"
|
||||
|
||||
|
||||
def test_msgpack_nested_pydantic_serializes_as_dict(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Nested Pydantic models are serialized via model_dump() as dicts.
|
||||
|
||||
This means nested models don't go through the ext hook and don't need
|
||||
to be in the allowlist - only the outer type does.
|
||||
"""
|
||||
|
||||
# Only allow outer type - inner is serialized as dict via model_dump()
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[("tests.test_jsonplus", "MyPydantic")]
|
||||
)
|
||||
|
||||
obj = MyPydantic(foo="test", bar=42, inner=InnerPydantic(hello="world"))
|
||||
|
||||
caplog.clear()
|
||||
dumped = serde.dumps_typed(obj)
|
||||
result = serde.loads_typed(dumped)
|
||||
|
||||
# No blocking should occur - inner is serialized as dict, not ext
|
||||
assert "blocked" not in caplog.text.lower()
|
||||
assert result == obj
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
@@ -10,6 +12,11 @@ from langgraph.checkpoint.base import (
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
|
||||
class MemoryPydantic(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
class TestMemorySaver:
|
||||
@@ -199,3 +206,105 @@ async def test_memory_saver() -> None:
|
||||
|
||||
with memory_saver as sync_memory_saver:
|
||||
assert sync_memory_saver is memory_saver
|
||||
|
||||
|
||||
def test_memory_saver_warns_on_unregistered_msgpack(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
memory_saver = InMemorySaver(serde=serde)
|
||||
obj = MemoryPydantic(foo="bar")
|
||||
|
||||
checkpoint = empty_checkpoint()
|
||||
checkpoint["channel_values"] = {"foo": obj}
|
||||
checkpoint["channel_versions"] = {"foo": 1}
|
||||
|
||||
config: RunnableConfig = {
|
||||
"configurable": {"thread_id": "thread-1", "checkpoint_ns": ""}
|
||||
}
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
new_config = memory_saver.put(config, checkpoint, {}, {"foo": 1})
|
||||
result = memory_saver.get_tuple(new_config)
|
||||
|
||||
assert result is not None
|
||||
assert "unregistered type" in caplog.text.lower()
|
||||
assert result.checkpoint["channel_values"]["foo"] == obj
|
||||
|
||||
|
||||
def test_memory_saver_allowlist_silences_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
serde = JsonPlusSerializer(
|
||||
allowed_msgpack_modules=[("tests.test_memory", "MemoryPydantic")]
|
||||
)
|
||||
memory_saver = InMemorySaver(serde=serde)
|
||||
obj = MemoryPydantic(foo="bar")
|
||||
|
||||
checkpoint = empty_checkpoint()
|
||||
checkpoint["channel_values"] = {"foo": obj}
|
||||
checkpoint["channel_versions"] = {"foo": 1}
|
||||
|
||||
config: RunnableConfig = {
|
||||
"configurable": {"thread_id": "thread-1", "checkpoint_ns": ""}
|
||||
}
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
new_config = memory_saver.put(config, checkpoint, {}, {"foo": 1})
|
||||
result = memory_saver.get_tuple(new_config)
|
||||
|
||||
assert result is not None
|
||||
assert "unregistered type" not in caplog.text.lower()
|
||||
assert result.checkpoint["channel_values"]["foo"] == obj
|
||||
|
||||
|
||||
def test_memory_saver_strict_blocks_unregistered(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
memory_saver = InMemorySaver(serde=serde)
|
||||
obj = MemoryPydantic(foo="bar")
|
||||
|
||||
checkpoint = empty_checkpoint()
|
||||
checkpoint["channel_values"] = {"foo": obj}
|
||||
checkpoint["channel_versions"] = {"foo": 1}
|
||||
|
||||
config: RunnableConfig = {
|
||||
"configurable": {"thread_id": "thread-1", "checkpoint_ns": ""}
|
||||
}
|
||||
|
||||
caplog.set_level(logging.WARNING, logger="langgraph.checkpoint.serde.jsonplus")
|
||||
new_config = memory_saver.put(config, checkpoint, {}, {"foo": 1})
|
||||
result = memory_saver.get_tuple(new_config)
|
||||
|
||||
assert result is not None
|
||||
assert "blocked" in caplog.text.lower()
|
||||
expected = obj.model_dump() if hasattr(obj, "model_dump") else obj.dict()
|
||||
assert result.checkpoint["channel_values"]["foo"] == expected
|
||||
|
||||
|
||||
def test_memory_saver_with_allowlist_proxy_isolated() -> None:
|
||||
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
memory_saver = InMemorySaver(serde=serde)
|
||||
proxy = memory_saver.with_allowlist([("tests.test_memory", "MemoryPydantic")])
|
||||
|
||||
obj = MemoryPydantic(foo="bar")
|
||||
|
||||
checkpoint = empty_checkpoint()
|
||||
checkpoint["channel_values"] = {"foo": obj}
|
||||
checkpoint["channel_versions"] = {"foo": 1}
|
||||
|
||||
config: RunnableConfig = {
|
||||
"configurable": {"thread_id": "thread-1", "checkpoint_ns": ""}
|
||||
}
|
||||
|
||||
new_config = proxy.put(config, checkpoint, {}, {"foo": 1})
|
||||
|
||||
proxied = proxy.get_tuple(new_config)
|
||||
assert proxied is not None
|
||||
assert proxied.checkpoint["channel_values"]["foo"] == obj
|
||||
|
||||
direct = memory_saver.get_tuple(new_config)
|
||||
assert direct is not None
|
||||
expected = obj.model_dump() if hasattr(obj, "model_dump") else obj.dict()
|
||||
assert direct.checkpoint["channel_values"]["foo"] == expected
|
||||
|
||||
Generated
+60
-23
@@ -267,7 +267,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.13"
|
||||
version = "1.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -279,14 +279,14 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/bb/c501ca60556c11ac80d1454bdcac63cb33583ce4e64fc4535ad5a7d5c6ba/langchain_core-1.2.13.tar.gz", hash = "sha256:d2773d0d0130a356378db9a858cfeef64c3d64bc03722f1d4d6c40eb46fdf01b", size = 831612, upload-time = "2026-02-15T07:45:57.014Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/ff/c5e3da8eca8a18719b300ef6c29e28208ee4e9da7f9749022b96292b6541/langchain_core-1.2.14.tar.gz", hash = "sha256:09549d838a2672781da3a9502f3b9c300863284b77b27e2a6dac4e6e650acfed", size = 833399, upload-time = "2026-02-19T14:22:33.514Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/ab/60fd69e5d55f67d422baefddaaca523c42cd7510ab6aeb17db6ae57fb107/langchain_core-1.2.13-py3-none-any.whl", hash = "sha256:b31823e28d3eff1e237096d0bd3bf80c6f9624eb471a9496dbfbd427779f8d82", size = 500485, upload-time = "2026-02-15T07:45:55.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/41/fe6ae9065b866b1397adbfc98db5e1648e8dcd78126b8e1266fcbe2d6395/langchain_core-1.2.14-py3-none-any.whl", hash = "sha256:b349ca28c057ac1f9b5280ea091bddb057db24d0f1c3c89bbb590713e1715838", size = 501411, upload-time = "2026-02-19T14:22:32.013Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -302,6 +302,7 @@ dev = [
|
||||
{ name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -341,6 +342,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -912,6 +914,41 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -1237,27 +1274,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
.PHONY: test lint format test-integration update-schema bump-version
|
||||
.PHONY: test lint type format test-integration update-schema bump-version
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -29,6 +29,9 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -20,6 +20,7 @@ from langgraph_cli.schemas import (
|
||||
Config,
|
||||
ConfigurableHeaderConfig,
|
||||
CorsConfig,
|
||||
GraphDef,
|
||||
HttpConfig,
|
||||
IndexConfig,
|
||||
SecurityConfig,
|
||||
@@ -108,6 +109,7 @@ def add_descriptions_to_schema(schema, cls):
|
||||
# Find the class that corresponds to this definition
|
||||
for potential_cls in [
|
||||
Config,
|
||||
GraphDef,
|
||||
StoreConfig,
|
||||
IndexConfig,
|
||||
AuthConfig,
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"test:all": "yarn test && yarn test:int && yarn lint:langgraph"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.1.24",
|
||||
"@langchain/langgraph": "^1.1.4"
|
||||
"@langchain/core": "^1.1.27",
|
||||
"@langchain/langgraph": "^1.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint": "^10.0.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-no-instanceof": "^1.0.1",
|
||||
|
||||
@@ -474,14 +474,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||
|
||||
"@eslint/config-array@^0.23.0":
|
||||
version "0.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.1.tgz#908223da7b9148f1af5bfb3144b77a9387a89446"
|
||||
integrity sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==
|
||||
"@eslint/config-array@^0.23.2":
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.2.tgz#db85beeff7facc685a5775caacb1c845669b9470"
|
||||
integrity sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^3.0.1"
|
||||
"@eslint/object-schema" "^3.0.2"
|
||||
debug "^4.3.1"
|
||||
minimatch "^10.1.1"
|
||||
minimatch "^10.2.1"
|
||||
|
||||
"@eslint/config-helpers@^0.5.2":
|
||||
version "0.5.2"
|
||||
@@ -517,10 +517,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583"
|
||||
integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==
|
||||
|
||||
"@eslint/object-schema@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.1.tgz#9a1dc9af00d790dc79a9bf57a756e3cb2740ddb9"
|
||||
integrity sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==
|
||||
"@eslint/object-schema@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.2.tgz#c59c6a94aa4b428ed7f1615b6a4495c0a21f7a22"
|
||||
integrity sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==
|
||||
|
||||
"@eslint/plugin-kit@^0.6.0":
|
||||
version "0.6.0"
|
||||
@@ -867,10 +867,10 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@langchain/core@^1.1.24":
|
||||
version "1.1.24"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.24.tgz#714f5d953c3887104616386a404fa5d5d29e8d9b"
|
||||
integrity sha512-u6l0dmMHN/2PCsY6stXoh9CH1OTlVR5Gjz0JjT1XRPuidAlu3kTq4ivW95xCog/PRhiAsCh6GCEC4/PqhNrcgQ==
|
||||
"@langchain/core@^1.1.27":
|
||||
version "1.1.27"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.27.tgz#b5a05c014eef2973006fd9e0df1e135b9da640e2"
|
||||
integrity sha512-YVtEz3nqCh8WxtdVXUICmt2BR2An+mn4YRJUBwcHX47Yrh2VwxpO0l97B2N/sNi658m65HnGyz2/hAjF3fzc1w==
|
||||
dependencies:
|
||||
"@cfworker/json-schema" "^4.0.2"
|
||||
ansi-styles "^5.0.0"
|
||||
@@ -890,23 +890,23 @@
|
||||
dependencies:
|
||||
uuid "^10.0.0"
|
||||
|
||||
"@langchain/langgraph-sdk@~1.6.0":
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.6.2.tgz#3b5d0707ddb844fd288cfacb81bb1b726ea7ee5f"
|
||||
integrity sha512-UzRZsnDqdTmeitf/K5yZnVdl+V+7bDj/hQUXm+Y8TwWUuKtWUDocIReKgAmPQLoIz0AN8bOUt0QGnIISmCZyuA==
|
||||
"@langchain/langgraph-sdk@~2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-2.0.0.tgz#55ac46373aa4917443d92c8b2edd5efd162bc879"
|
||||
integrity sha512-Xdkl1hve84ZGQ7fgpiBIBvjODhtjbPPccY4snOtYgSdzRXZkESsi2Y7RDKgFe1nC9+DbX+QaYom0raD/XFBKAw==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
p-queue "^9.0.1"
|
||||
p-retry "^7.1.1"
|
||||
uuid "^13.0.0"
|
||||
|
||||
"@langchain/langgraph@^1.1.4":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.1.4.tgz#9eb2ca3bf03329a16d0bd8ae734fdd139d5862f8"
|
||||
integrity sha512-9OhRF+7Zvcpure8TLtBrxfJDo0PAoHZhfzcPL6M3CsGXiYqLWm5tQe+FYqn9zRIV7IwphqVEl1QDNbOkVgo+kw==
|
||||
"@langchain/langgraph@^1.1.5":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.1.5.tgz#7cab6c585b5e60ac52e70ef941ba6054f45b0d88"
|
||||
integrity sha512-uJC/asydf/GoHpo9x42lf9hs8ufCkMuJ9sDle5ybP7sMD0XryOfE0E4J3deARk9ZadCCt6zeCoCNu/mTbx8+Sg==
|
||||
dependencies:
|
||||
"@langchain/langgraph-checkpoint" "^1.0.0"
|
||||
"@langchain/langgraph-sdk" "~1.6.0"
|
||||
"@langchain/langgraph-sdk" "~2.0.0"
|
||||
"@standard-schema/spec" "1.1.0"
|
||||
uuid "^10.0.0"
|
||||
|
||||
@@ -1287,10 +1287,10 @@ acorn@^8.12.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
|
||||
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
|
||||
|
||||
acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
acorn@^8.16.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
@@ -2226,10 +2226,10 @@ eslint-plugin-prettier@^5.5.5:
|
||||
prettier-linter-helpers "^1.0.1"
|
||||
synckit "^0.11.12"
|
||||
|
||||
eslint-scope@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.0.tgz#dfcb41d6c0d73df6b977a50cf3e91c41ddb4154e"
|
||||
integrity sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==
|
||||
eslint-scope@^9.1.1:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.1.tgz#f6a209486e38bd28356b5feb07d445cc99c89967"
|
||||
integrity sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==
|
||||
dependencies:
|
||||
"@types/esrecurse" "^4.3.1"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -2246,19 +2246,19 @@ eslint-visitor-keys@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint-visitor-keys@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9"
|
||||
integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==
|
||||
eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.0.tgz#c93c36a96d91621d0fbb680db848ea11af56ab1e"
|
||||
integrity sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==
|
||||
eslint@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.1.tgz#b5c5f7706782a21590ba6451e7a30d2947273c2d"
|
||||
integrity sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@eslint/config-array" "^0.23.0"
|
||||
"@eslint/config-array" "^0.23.2"
|
||||
"@eslint/config-helpers" "^0.5.2"
|
||||
"@eslint/core" "^1.1.0"
|
||||
"@eslint/plugin-kit" "^0.6.0"
|
||||
@@ -2270,9 +2270,9 @@ eslint@^10.0.0:
|
||||
cross-spawn "^7.0.6"
|
||||
debug "^4.3.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^9.1.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
espree "^11.1.0"
|
||||
eslint-scope "^9.1.1"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
espree "^11.1.1"
|
||||
esquery "^1.7.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
@@ -2283,7 +2283,7 @@ eslint@^10.0.0:
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
minimatch "^10.1.1"
|
||||
minimatch "^10.2.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
|
||||
@@ -2296,14 +2296,14 @@ espree@^10.0.1:
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^4.0.0"
|
||||
|
||||
espree@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.0.tgz#7d0c82a69f8df670728dba256264b383fbf73e8f"
|
||||
integrity sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==
|
||||
espree@^11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.1.tgz#866f6bc9ccccd6f28876b7a6463abb281b9cb847"
|
||||
integrity sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==
|
||||
dependencies:
|
||||
acorn "^8.15.0"
|
||||
acorn "^8.16.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
@@ -3533,9 +3533,9 @@ js-tokens@^4.0.0:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^3.13.1:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||
version "3.14.2"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0"
|
||||
integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
@@ -3699,10 +3699,10 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
minimatch@^10.1.1:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.0.tgz#e710473e66e3e1aaf376d0aa82438375cac86e9e"
|
||||
integrity sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==
|
||||
minimatch@^10.2.1:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
|
||||
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-monorepo-example/shared": "*",
|
||||
"@langchain/core": "^1.1.24",
|
||||
"@langchain/langgraph": "^1.1.4"
|
||||
"@langchain/core": "^1.1.27",
|
||||
"@langchain/langgraph": "^1.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
"lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.8.9",
|
||||
"turbo": "^2.8.10",
|
||||
"typescript": "^5.9.3",
|
||||
"@tsconfig/recommended": "^1.0.13",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint": "^10.0.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-no-instanceof": "^1.0.1",
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||
|
||||
"@eslint/config-array@^0.23.0":
|
||||
version "0.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.1.tgz#908223da7b9148f1af5bfb3144b77a9387a89446"
|
||||
integrity sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==
|
||||
"@eslint/config-array@^0.23.2":
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.2.tgz#db85beeff7facc685a5775caacb1c845669b9470"
|
||||
integrity sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^3.0.1"
|
||||
"@eslint/object-schema" "^3.0.2"
|
||||
debug "^4.3.1"
|
||||
minimatch "^10.1.1"
|
||||
minimatch "^10.2.1"
|
||||
|
||||
"@eslint/config-helpers@^0.5.2":
|
||||
version "0.5.2"
|
||||
@@ -62,10 +62,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583"
|
||||
integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==
|
||||
|
||||
"@eslint/object-schema@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.1.tgz#9a1dc9af00d790dc79a9bf57a756e3cb2740ddb9"
|
||||
integrity sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==
|
||||
"@eslint/object-schema@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.2.tgz#c59c6a94aa4b428ed7f1615b6a4495c0a21f7a22"
|
||||
integrity sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==
|
||||
|
||||
"@eslint/plugin-kit@^0.6.0":
|
||||
version "0.6.0"
|
||||
@@ -103,10 +103,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-9.0.0.tgz#4d0a3f127058043bf2e7ee169eaf30ed901302f3"
|
||||
integrity sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==
|
||||
|
||||
"@langchain/core@^1.1.24":
|
||||
version "1.1.24"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.24.tgz#714f5d953c3887104616386a404fa5d5d29e8d9b"
|
||||
integrity sha512-u6l0dmMHN/2PCsY6stXoh9CH1OTlVR5Gjz0JjT1XRPuidAlu3kTq4ivW95xCog/PRhiAsCh6GCEC4/PqhNrcgQ==
|
||||
"@langchain/core@^1.1.27":
|
||||
version "1.1.27"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.27.tgz#b5a05c014eef2973006fd9e0df1e135b9da640e2"
|
||||
integrity sha512-YVtEz3nqCh8WxtdVXUICmt2BR2An+mn4YRJUBwcHX47Yrh2VwxpO0l97B2N/sNi658m65HnGyz2/hAjF3fzc1w==
|
||||
dependencies:
|
||||
"@cfworker/json-schema" "^4.0.2"
|
||||
ansi-styles "^5.0.0"
|
||||
@@ -126,23 +126,23 @@
|
||||
dependencies:
|
||||
uuid "^10.0.0"
|
||||
|
||||
"@langchain/langgraph-sdk@~1.6.0":
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.6.2.tgz#3b5d0707ddb844fd288cfacb81bb1b726ea7ee5f"
|
||||
integrity sha512-UzRZsnDqdTmeitf/K5yZnVdl+V+7bDj/hQUXm+Y8TwWUuKtWUDocIReKgAmPQLoIz0AN8bOUt0QGnIISmCZyuA==
|
||||
"@langchain/langgraph-sdk@~2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-2.0.0.tgz#55ac46373aa4917443d92c8b2edd5efd162bc879"
|
||||
integrity sha512-Xdkl1hve84ZGQ7fgpiBIBvjODhtjbPPccY4snOtYgSdzRXZkESsi2Y7RDKgFe1nC9+DbX+QaYom0raD/XFBKAw==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
p-queue "^9.0.1"
|
||||
p-retry "^7.1.1"
|
||||
uuid "^13.0.0"
|
||||
|
||||
"@langchain/langgraph@^1.1.4":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.1.4.tgz#9eb2ca3bf03329a16d0bd8ae734fdd139d5862f8"
|
||||
integrity sha512-9OhRF+7Zvcpure8TLtBrxfJDo0PAoHZhfzcPL6M3CsGXiYqLWm5tQe+FYqn9zRIV7IwphqVEl1QDNbOkVgo+kw==
|
||||
"@langchain/langgraph@^1.1.5":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.1.5.tgz#7cab6c585b5e60ac52e70ef941ba6054f45b0d88"
|
||||
integrity sha512-uJC/asydf/GoHpo9x42lf9hs8ufCkMuJ9sDle5ybP7sMD0XryOfE0E4J3deARk9ZadCCt6zeCoCNu/mTbx8+Sg==
|
||||
dependencies:
|
||||
"@langchain/langgraph-checkpoint" "^1.0.0"
|
||||
"@langchain/langgraph-sdk" "~1.6.0"
|
||||
"@langchain/langgraph-sdk" "~2.0.0"
|
||||
"@standard-schema/spec" "1.1.0"
|
||||
uuid "^10.0.0"
|
||||
|
||||
@@ -297,6 +297,11 @@ acorn@^8.15.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
|
||||
acorn@^8.16.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
@@ -772,10 +777,10 @@ eslint-plugin-prettier@^5.5.5:
|
||||
prettier-linter-helpers "^1.0.1"
|
||||
synckit "^0.11.12"
|
||||
|
||||
eslint-scope@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.0.tgz#dfcb41d6c0d73df6b977a50cf3e91c41ddb4154e"
|
||||
integrity sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==
|
||||
eslint-scope@^9.1.1:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.1.tgz#f6a209486e38bd28356b5feb07d445cc99c89967"
|
||||
integrity sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==
|
||||
dependencies:
|
||||
"@types/esrecurse" "^4.3.1"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -792,19 +797,19 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint-visitor-keys@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9"
|
||||
integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==
|
||||
eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.0.tgz#c93c36a96d91621d0fbb680db848ea11af56ab1e"
|
||||
integrity sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==
|
||||
eslint@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.1.tgz#b5c5f7706782a21590ba6451e7a30d2947273c2d"
|
||||
integrity sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@eslint/config-array" "^0.23.0"
|
||||
"@eslint/config-array" "^0.23.2"
|
||||
"@eslint/config-helpers" "^0.5.2"
|
||||
"@eslint/core" "^1.1.0"
|
||||
"@eslint/plugin-kit" "^0.6.0"
|
||||
@@ -816,9 +821,9 @@ eslint@^10.0.0:
|
||||
cross-spawn "^7.0.6"
|
||||
debug "^4.3.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^9.1.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
espree "^11.1.0"
|
||||
eslint-scope "^9.1.1"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
espree "^11.1.1"
|
||||
esquery "^1.7.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
@@ -829,7 +834,7 @@ eslint@^10.0.0:
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
minimatch "^10.1.1"
|
||||
minimatch "^10.2.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
|
||||
@@ -842,14 +847,14 @@ espree@^10.0.1:
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
espree@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.0.tgz#7d0c82a69f8df670728dba256264b383fbf73e8f"
|
||||
integrity sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==
|
||||
espree@^11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.1.tgz#866f6bc9ccccd6f28876b7a6463abb281b9cb847"
|
||||
integrity sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==
|
||||
dependencies:
|
||||
acorn "^8.15.0"
|
||||
acorn "^8.16.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
|
||||
esquery@^1.7.0:
|
||||
version "1.7.0"
|
||||
@@ -1374,10 +1379,10 @@ math-intrinsics@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
minimatch@^10.1.1:
|
||||
version "10.2.1"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.1.tgz#9d82835834cdc85d5084dd055e9a4685fa56e5f0"
|
||||
integrity sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==
|
||||
minimatch@^10.2.1:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
|
||||
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
@@ -1858,47 +1863,47 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
turbo-darwin-64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.8.9.tgz#c99b425437c8b3b49eb4ddf7ca44674dd6d4e940"
|
||||
integrity sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw==
|
||||
turbo-darwin-64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.8.10.tgz#38ba73b9bbb3459cd27c6d3b5777bae80daa7e9f"
|
||||
integrity sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g==
|
||||
|
||||
turbo-darwin-arm64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.9.tgz#01a424a14956186bdd3a29a825f87c303ecbfda1"
|
||||
integrity sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA==
|
||||
turbo-darwin-arm64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.10.tgz#1c7a278a6361aef0a4e94849bf7181ff3c91d782"
|
||||
integrity sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA==
|
||||
|
||||
turbo-linux-64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.8.9.tgz#f8e7f4bf82a2f94a3d8bcd042642db4990cfd3bd"
|
||||
integrity sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg==
|
||||
turbo-linux-64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.8.10.tgz#aaa6ede45619daab2be359ec6afc3b0a73941272"
|
||||
integrity sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA==
|
||||
|
||||
turbo-linux-arm64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.8.9.tgz#7e7cd446c518dd4e19aa87f71c3234124449681d"
|
||||
integrity sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug==
|
||||
turbo-linux-arm64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.8.10.tgz#a9a52e4eca69968d85f09adb24ba3aec24a50023"
|
||||
integrity sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ==
|
||||
|
||||
turbo-windows-64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.8.9.tgz#71da356ba3a9585ddbf3eb6f85a92239454aa9bd"
|
||||
integrity sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg==
|
||||
turbo-windows-64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.8.10.tgz#b0f64a29451477c1ecdc7dbe0555b8e75c464044"
|
||||
integrity sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw==
|
||||
|
||||
turbo-windows-arm64@2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.8.9.tgz#b1c223c1f2c900292246074c3c2ebc9c7db57684"
|
||||
integrity sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg==
|
||||
turbo-windows-arm64@2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.8.10.tgz#8d178389a995f98142b7ce8f76e1fce8a4b7c79d"
|
||||
integrity sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ==
|
||||
|
||||
turbo@^2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.8.9.tgz#15d8468f9f1725381a9bab58ccdb12c5614de5bc"
|
||||
integrity sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g==
|
||||
turbo@^2.8.10:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.8.10.tgz#4ead3ef7c2fd80f9fe367f9a8b88b8a819fb5065"
|
||||
integrity sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "2.8.9"
|
||||
turbo-darwin-arm64 "2.8.9"
|
||||
turbo-linux-64 "2.8.9"
|
||||
turbo-linux-arm64 "2.8.9"
|
||||
turbo-windows-64 "2.8.9"
|
||||
turbo-windows-arm64 "2.8.9"
|
||||
turbo-darwin-64 "2.8.10"
|
||||
turbo-darwin-arm64 "2.8.10"
|
||||
turbo-linux-64 "2.8.10"
|
||||
turbo-linux-arm64 "2.8.10"
|
||||
turbo-windows-64 "2.8.10"
|
||||
turbo-windows-arm64 "2.8.10"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
|
||||
@@ -678,6 +678,51 @@ def _update_encryption_path(
|
||||
)
|
||||
|
||||
|
||||
def _update_checkpointer_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
"""Update checkpointer.path to use Docker container paths."""
|
||||
checkpointer_conf = config.get("checkpointer")
|
||||
if not checkpointer_conf or not isinstance(checkpointer_conf, dict):
|
||||
return
|
||||
if not (path_str := checkpointer_conf.get("path")):
|
||||
return
|
||||
|
||||
module_str, sep, attr_str = path_str.partition(":")
|
||||
if not sep or not module_str.startswith("."):
|
||||
return # Already validated or absolute path
|
||||
|
||||
resolved = config_path.parent / module_str
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Checkpointer file not found: {resolved} (from {path_str})"
|
||||
)
|
||||
if not resolved.is_file():
|
||||
raise IsADirectoryError(f"Checkpointer path must be a file: {resolved}")
|
||||
|
||||
# Check faux packages first (higher priority)
|
||||
for faux_path, (_, destpath) in local_deps.faux_pkgs.items():
|
||||
if resolved.is_relative_to(faux_path):
|
||||
new_path = f"{destpath}/{resolved.relative_to(faux_path)}:{attr_str}"
|
||||
checkpointer_conf["path"] = new_path
|
||||
return
|
||||
|
||||
# Check real packages
|
||||
for real_path in local_deps.real_pkgs:
|
||||
if resolved.is_relative_to(real_path):
|
||||
new_path = (
|
||||
f"/deps/{real_path.name}/{resolved.relative_to(real_path)}:{attr_str}"
|
||||
)
|
||||
checkpointer_conf["path"] = new_path
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Checkpointer file '{resolved}' not covered by dependencies.\n"
|
||||
"Add its parent directory to the 'dependencies' array in your config.\n"
|
||||
f"Current dependencies: {config['dependencies']}"
|
||||
)
|
||||
|
||||
|
||||
def _update_http_app_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
@@ -877,6 +922,8 @@ def python_config_to_docker(
|
||||
_update_auth_path(config_path, config, local_deps)
|
||||
# Rewrite encryption path, so it points to the correct location in the Docker container
|
||||
_update_encryption_path(config_path, config, local_deps)
|
||||
# Rewrite checkpointer path, so it points to the correct location in the Docker container
|
||||
_update_checkpointer_path(config_path, config, local_deps)
|
||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||
_update_http_app_path(config_path, config, local_deps)
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class SerdeConfig(TypedDict, total=False):
|
||||
If omitted, no serde is set up (the object store will still be present, however)."""
|
||||
|
||||
allowed_json_modules: list[list[str]] | bool | None
|
||||
"""Optional. List of allowed python modules to de-serialize custom objects from.
|
||||
"""Optional. List of allowed python modules to de-serialize custom objects from JSON.
|
||||
|
||||
If provided, only the specified modules will be allowed to be deserialized.
|
||||
If omitted, no modules are allowed, and the object returned will simply be a json object OR
|
||||
@@ -148,7 +148,34 @@ class SerdeConfig(TypedDict, total=False):
|
||||
Example:
|
||||
{...
|
||||
"serde": {
|
||||
"allowed_json_modules": true
|
||||
"allowed_json_modules": True
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
allowed_msgpack_modules: list[list[str]] | bool | None
|
||||
"""Optional. List of allowed python modules to de-serialize custom objects from msgpack.
|
||||
|
||||
Known safe types (langgraph.checkpoint.serde.jsonplus.SAFE_MSGPACK_TYPES) are always
|
||||
allowed regardless of this setting. Use this to allowlist your custom Pydantic models,
|
||||
dataclasses, and other user-defined types.
|
||||
|
||||
If True (default), unregistered types will log a warning but still be deserialized.
|
||||
If None, only known safe types will be deserialized; unregistered types will be blocked.
|
||||
|
||||
Example - allowlist specific types (no warnings for these):
|
||||
{...
|
||||
"serde": {
|
||||
"allowed_msgpack_modules": [
|
||||
["my_agent.models", "MyState"],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Example - strict mode (only safe types allowed):
|
||||
{...
|
||||
"serde": {
|
||||
"allowed_msgpack_modules": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +194,26 @@ class CheckpointerConfig(TypedDict, total=False):
|
||||
If omitted, no checkpointer is set up (the object store will still be present, however).
|
||||
"""
|
||||
|
||||
path: str
|
||||
"""Import path to an async context manager that yields a `BaseCheckpointSaver`
|
||||
instance.
|
||||
|
||||
The referenced object should be an `@asynccontextmanager`-decorated function
|
||||
so that the server can properly manage the checkpointer's lifecycle (e.g.
|
||||
opening and closing connections).
|
||||
|
||||
Examples:
|
||||
- "./my_checkpointer.py:create_checkpointer"
|
||||
- "my_package.checkpointer:create_checkpointer"
|
||||
|
||||
When provided, this replaces the default checkpointer.
|
||||
|
||||
You can use the `langgraph-checkpoint-conformance` package
|
||||
(https://pypi.org/project/langgraph-checkpoint-conformance/) to run simple
|
||||
conformance tests against your custom checkpointer and catch
|
||||
incompatibilities early.
|
||||
"""
|
||||
|
||||
ttl: ThreadTTLConfig | None
|
||||
"""Optional. Defines the TTL (time-to-live) behavior configuration.
|
||||
|
||||
@@ -308,8 +355,7 @@ class EncryptionConfig(TypedDict, total=False):
|
||||
"""Configuration for custom at-rest encryption logic.
|
||||
|
||||
Allows you to implement custom encryption for sensitive data stored in the database,
|
||||
including metadata fields and checkpoint blobs.
|
||||
"""
|
||||
including metadata fields and checkpoint blobs."""
|
||||
|
||||
path: str
|
||||
"""Required. Path to an instance of the Encryption() class that implements custom encryption handlers.
|
||||
@@ -509,6 +555,21 @@ class WebhookUrlPolicy(TypedDict, total=False):
|
||||
"""Disallow relative URLs (internal loopback calls) when true."""
|
||||
|
||||
|
||||
class GraphDef(TypedDict, total=False):
|
||||
"""Definition of a graph with additional metadata."""
|
||||
|
||||
path: str
|
||||
"""Required. Import path to the graph object.
|
||||
|
||||
Format: "path/to/file.py:object_name"
|
||||
"""
|
||||
description: str | None
|
||||
"""Optional. A description of the graph's purpose and functionality.
|
||||
|
||||
This description is surfaced in the API and can help users understand what the graph does.
|
||||
"""
|
||||
|
||||
|
||||
class WebhooksConfig(TypedDict, total=False):
|
||||
env_prefix: str
|
||||
"""Required prefix for environment variables referenced in header templates.
|
||||
@@ -599,19 +660,23 @@ class Config(TypedDict, total=False):
|
||||
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
|
||||
"""
|
||||
|
||||
graphs: dict[str, str]
|
||||
graphs: dict[str, str | GraphDef]
|
||||
"""Optional. Named definitions of graphs, each pointing to a Python object.
|
||||
|
||||
|
||||
|
||||
Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
|
||||
managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
|
||||
(instance of Stategraph, etc.).
|
||||
|
||||
Keys are graph names, values are "path/to/file.py:object_name".
|
||||
|
||||
Keys are graph names, values are either "path/to/file.py:object_name" strings
|
||||
or objects with a "path" key and optional "description" key.
|
||||
Example:
|
||||
{
|
||||
"mygraph": "graphs/my_graph.py:graph_definition",
|
||||
"anothergraph": "graphs/another.py:get_graph"
|
||||
"anothergraph": {
|
||||
"path": "graphs/another.py:get_graph",
|
||||
"description": "A graph that does X"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -676,6 +741,7 @@ class Config(TypedDict, total=False):
|
||||
|
||||
__all__ = [
|
||||
"Config",
|
||||
"GraphDef",
|
||||
"StoreConfig",
|
||||
"CheckpointerConfig",
|
||||
"AuthConfig",
|
||||
|
||||
@@ -127,9 +127,16 @@
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GraphDef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n\nor objects with a \"path\" key and optional \"description\" key."
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
@@ -341,9 +348,16 @@
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GraphDef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n\nor objects with a \"path\" key and optional \"description\" key."
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
@@ -542,6 +556,10 @@
|
||||
"description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Import path to an async context manager that yields a `BaseCheckpointSaver`\ninstance.\n\nThe referenced object should be an `@asynccontextmanager`-decorated function\nso that the server can properly manage the checkpointer's lifecycle (e.g.\nopening and closing connections).\n"
|
||||
},
|
||||
"serde": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -590,7 +608,27 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from JSON.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
|
||||
},
|
||||
"allowed_msgpack_modules": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from msgpack.\n\nKnown safe types (langgraph.checkpoint.serde.jsonplus.SAFE_MSGPACK_TYPES) are always\nallowed regardless of this setting. Use this to allowlist your custom Pydantic models,\ndataclasses, and other user-defined types.\n\nIf True (default), unregistered types will log a warning but still be deserialized.\nIf None, only known safe types will be deserialized; unregistered types will be blocked.\n\n{...\n[\"my_agent.models\", \"MyState\"],\n]\n}\n}\n\n{...\n}\n}\n\n"
|
||||
},
|
||||
"pickle_fallback": {
|
||||
"type": "boolean",
|
||||
@@ -658,6 +696,29 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"GraphDef": {
|
||||
"title": "GraphDef",
|
||||
"description": "Definition of a graph with additional metadata.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. A description of the graph's purpose and functionality.\n\nThis description is surfaced in the API and can help users understand what the graph does.\n"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Import path to the graph object.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"HttpConfig": {
|
||||
"title": "HttpConfig",
|
||||
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
|
||||
|
||||
@@ -127,9 +127,16 @@
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GraphDef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n\nor objects with a \"path\" key and optional \"description\" key."
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
@@ -341,9 +348,16 @@
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GraphDef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n\nor objects with a \"path\" key and optional \"description\" key."
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
@@ -542,6 +556,10 @@
|
||||
"description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Import path to an async context manager that yields a `BaseCheckpointSaver`\ninstance.\n\nThe referenced object should be an `@asynccontextmanager`-decorated function\nso that the server can properly manage the checkpointer's lifecycle (e.g.\nopening and closing connections).\n"
|
||||
},
|
||||
"serde": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -590,7 +608,27 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from JSON.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
|
||||
},
|
||||
"allowed_msgpack_modules": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of allowed python modules to de-serialize custom objects from msgpack.\n\nKnown safe types (langgraph.checkpoint.serde.jsonplus.SAFE_MSGPACK_TYPES) are always\nallowed regardless of this setting. Use this to allowlist your custom Pydantic models,\ndataclasses, and other user-defined types.\n\nIf True (default), unregistered types will log a warning but still be deserialized.\nIf None, only known safe types will be deserialized; unregistered types will be blocked.\n\n{...\n[\"my_agent.models\", \"MyState\"],\n]\n}\n}\n\n{...\n}\n}\n\n"
|
||||
},
|
||||
"pickle_fallback": {
|
||||
"type": "boolean",
|
||||
@@ -658,6 +696,29 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"GraphDef": {
|
||||
"title": "GraphDef",
|
||||
"description": "Definition of a graph with additional metadata.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. A description of the graph's purpose and functionality.\n\nThis description is surfaced in the API and can help users understand what the graph does.\n"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Import path to the graph object.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"HttpConfig": {
|
||||
"title": "HttpConfig",
|
||||
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
|
||||
|
||||
Generated
+22
-22
@@ -1071,15 +1071,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.3.6"
|
||||
version = "0.3.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "orjson", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/ec/477fa8b408f948b145d90fd935c0a9f37945fa5ec1dfabfc71e7cafba6d8/langgraph_sdk-0.3.6.tar.gz", hash = "sha256:7650f607f89c1586db5bee391b1a8754cbe1fc83b721ff2f1450f8906e790bd7", size = 182666, upload-time = "2026-02-14T19:46:03.752Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/22/f451b7f42e7c553f649c51698b5ff82ed1932993bcb9b7a7c53d888849e1/langgraph_sdk-0.3.8.tar.gz", hash = "sha256:e73e56e403254ebada5cab70165eb0b69155979e2360bca84da2cb63f364dfb9", size = 183804, upload-time = "2026-02-19T19:12:37.971Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/61/12508e12652edd1874327271a5a8834c728a605f53a1a1c945f13ab69664/langgraph_sdk-0.3.6-py3-none-any.whl", hash = "sha256:7df2fd552ad7262d0baf8e1f849dce1d62186e76dcdd36db9dc5bdfa5c3fc20f", size = 88277, upload-time = "2026-02-14T19:46:02.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/77/00887fb1fb2c0d61eed0dd76d1ed919558b679f71904d63de6925ca350f9/langgraph_sdk-0.3.8-py3-none-any.whl", hash = "sha256:90436594e95c6fc1d1dafb59ac1c5eff2f8e1853eecc6082262b8e6de04233c1", size = 90038, upload-time = "2026-02-19T19:12:36.65Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1962,27 +1962,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint test test_watch integration_tests spell_check spell_fix benchmark profile start-dev-server integration_tests
|
||||
.PHONY: all format lint type test test_watch integration_tests spell_check spell_fix benchmark profile start-dev-server integration_tests
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -87,15 +87,15 @@ integration_tests:
|
||||
|
||||
WORKERS ?= auto
|
||||
XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
|
||||
MAXFAIL ?=
|
||||
MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
|
||||
MAXFAIL ?= 1
|
||||
MAXFAIL_ARGS = $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
|
||||
# Add an '-x' if xdist is enabled
|
||||
XDIST_ARGS := $(if $(WORKERS),-x $(XDIST_ARGS),)
|
||||
|
||||
test_watch:
|
||||
make start-services &&\
|
||||
make start-dev-server &&\
|
||||
uv run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
|
||||
uv run ptw -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-dev-server; \
|
||||
@@ -125,9 +125,12 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run mypy langgraph --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy langgraph --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
uv run ruff check --fix $(PYTHON_FILES)
|
||||
|
||||
spell_check:
|
||||
uv run codespell --toml pyproject.toml
|
||||
@@ -147,6 +150,7 @@ help:
|
||||
@echo '-- LINTING --'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
@echo 'spell_check - run codespell on the project'
|
||||
@echo 'spell_fix - run codespell on the project and fix the errors'
|
||||
@echo '-- TESTS --'
|
||||
|
||||
@@ -10,6 +10,7 @@ from bench.fanout_to_subgraph import fanout_to_subgraph, fanout_to_subgraph_sync
|
||||
from bench.pydantic_state import pydantic_state
|
||||
from bench.react_agent import react_agent
|
||||
from bench.sequential import create_sequential
|
||||
from bench.serde_allowlist import collect_allowlist_large, collect_allowlist_small
|
||||
from bench.wide_dict import wide_dict
|
||||
from bench.wide_state import wide_state
|
||||
from langgraph.graph import StateGraph
|
||||
@@ -513,3 +514,7 @@ compilation_benchmarks = (
|
||||
|
||||
for name, graph in compilation_benchmarks:
|
||||
r.bench_func(name + "_compilation", compile_graph, graph)
|
||||
|
||||
# Serde allowlist collection
|
||||
r.bench_func("serde_allowlist_small", collect_allowlist_small)
|
||||
r.bench_func("serde_allowlist_large", collect_allowlist_large)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
from langgraph._internal._serde import collect_allowlist_from_schemas
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
RED = "red"
|
||||
BLUE = "blue"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InnerDataclass:
|
||||
value: int
|
||||
|
||||
|
||||
class InnerModel(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class InnerTyped(TypedDict):
|
||||
payload: InnerDataclass
|
||||
optional: NotRequired[InnerModel]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
value: int
|
||||
child: Node | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedDataclass:
|
||||
inner: InnerDataclass
|
||||
items: list[InnerModel]
|
||||
mapping: dict[str, InnerDataclass]
|
||||
optional: InnerModel | None
|
||||
union: InnerDataclass | InnerModel
|
||||
queue: deque[InnerDataclass]
|
||||
frozen: frozenset[InnerModel]
|
||||
|
||||
|
||||
AnnotatedList = Annotated[list[InnerDataclass], "meta"]
|
||||
|
||||
|
||||
class DummyChannel:
|
||||
@property
|
||||
def ValueType(self) -> type[InnerDataclass]:
|
||||
return InnerDataclass
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> type[InnerModel]:
|
||||
return InnerModel
|
||||
|
||||
|
||||
SCHEMAS_SMALL = [InnerDataclass, InnerModel, Color]
|
||||
SCHEMAS_LARGE = [
|
||||
InnerDataclass,
|
||||
InnerModel,
|
||||
Color,
|
||||
InnerTyped,
|
||||
Node,
|
||||
NestedDataclass,
|
||||
AnnotatedList,
|
||||
]
|
||||
CHANNELS = {"a": DummyChannel(), "b": DummyChannel()}
|
||||
|
||||
|
||||
def collect_allowlist_small() -> None:
|
||||
collect_allowlist_from_schemas(schemas=SCHEMAS_SMALL, channels=CHANNELS)
|
||||
|
||||
|
||||
def collect_allowlist_large() -> None:
|
||||
collect_allowlist_from_schemas(schemas=SCHEMAS_LARGE, channels=CHANNELS)
|
||||
@@ -0,0 +1,253 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from langchain_core import messages as lc_messages
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import NotRequired, Required, is_typeddict
|
||||
|
||||
try:
|
||||
from langgraph.checkpoint.serde._msgpack import ( # noqa: F401
|
||||
STRICT_MSGPACK_ENABLED,
|
||||
)
|
||||
except ImportError:
|
||||
STRICT_MSGPACK_ENABLED = False
|
||||
|
||||
_warned_allowlist_unsupported = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _supports_checkpointer_allowlist() -> bool:
|
||||
return hasattr(BaseCheckpointSaver, "with_allowlist")
|
||||
|
||||
|
||||
_SUPPORTS_ALLOWLIST = _supports_checkpointer_allowlist()
|
||||
|
||||
|
||||
def apply_checkpointer_allowlist(
|
||||
checkpointer: Any, allowlist: set[tuple[str, ...]] | None
|
||||
) -> Any:
|
||||
if not checkpointer or allowlist is None or checkpointer in (True, False):
|
||||
return checkpointer
|
||||
if not _SUPPORTS_ALLOWLIST:
|
||||
global _warned_allowlist_unsupported
|
||||
if not _warned_allowlist_unsupported:
|
||||
logger.warning(
|
||||
"Checkpointer does not support with_allowlist; strict msgpack "
|
||||
"allowlist will be skipped."
|
||||
)
|
||||
_warned_allowlist_unsupported = True
|
||||
return checkpointer
|
||||
return checkpointer.with_allowlist(allowlist)
|
||||
|
||||
|
||||
def curated_core_allowlist() -> set[tuple[str, ...]]:
|
||||
allowlist: set[tuple[str, ...]] = set()
|
||||
for name in (
|
||||
"BaseMessage",
|
||||
"BaseMessageChunk",
|
||||
"HumanMessage",
|
||||
"HumanMessageChunk",
|
||||
"AIMessage",
|
||||
"AIMessageChunk",
|
||||
"SystemMessage",
|
||||
"SystemMessageChunk",
|
||||
"ChatMessage",
|
||||
"ChatMessageChunk",
|
||||
"ToolMessage",
|
||||
"ToolMessageChunk",
|
||||
"FunctionMessage",
|
||||
"FunctionMessageChunk",
|
||||
"RemoveMessage",
|
||||
):
|
||||
cls = getattr(lc_messages, name, None)
|
||||
if cls is None:
|
||||
continue
|
||||
allowlist.add((cls.__module__, cls.__name__))
|
||||
|
||||
return allowlist
|
||||
|
||||
|
||||
def build_serde_allowlist(
|
||||
*,
|
||||
schemas: list[type[Any]] | None = None,
|
||||
channels: dict[str, Any] | None = None,
|
||||
) -> set[tuple[str, ...]]:
|
||||
allowlist = curated_core_allowlist()
|
||||
if schemas:
|
||||
schemas = [schema for schema in schemas if schema is not None]
|
||||
return allowlist | collect_allowlist_from_schemas(
|
||||
schemas=schemas,
|
||||
channels=channels,
|
||||
)
|
||||
|
||||
|
||||
def collect_allowlist_from_schemas(
|
||||
*,
|
||||
schemas: list[type[Any]] | None = None,
|
||||
channels: dict[str, Any] | None = None,
|
||||
) -> set[tuple[str, ...]]:
|
||||
allowlist: set[tuple[str, ...]] = set()
|
||||
seen: set[Any] = set()
|
||||
seen_ids: set[int] = set()
|
||||
|
||||
if schemas:
|
||||
for schema in schemas:
|
||||
_collect_from_type(schema, allowlist, seen, seen_ids)
|
||||
|
||||
if channels:
|
||||
for channel in channels.values():
|
||||
value_type = getattr(channel, "ValueType", None)
|
||||
if value_type is not None:
|
||||
_collect_from_type(value_type, allowlist, seen, seen_ids)
|
||||
update_type = getattr(channel, "UpdateType", None)
|
||||
if update_type is not None:
|
||||
_collect_from_type(update_type, allowlist, seen, seen_ids)
|
||||
|
||||
return allowlist
|
||||
|
||||
|
||||
def _collect_from_type(
|
||||
typ: Any,
|
||||
allowlist: set[tuple[str, ...]],
|
||||
seen: set[Any],
|
||||
seen_ids: set[int],
|
||||
) -> None:
|
||||
if _already_seen(typ, seen, seen_ids):
|
||||
return
|
||||
|
||||
if typ is Any or typ is None:
|
||||
return
|
||||
|
||||
if typ is Literal:
|
||||
return
|
||||
|
||||
if isinstance(typ, types.UnionType):
|
||||
for arg in typ.__args__:
|
||||
_collect_from_type(arg, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
origin = get_origin(typ)
|
||||
if origin is Union:
|
||||
for arg in get_args(typ):
|
||||
_collect_from_type(arg, allowlist, seen, seen_ids)
|
||||
return
|
||||
if origin is Annotated or origin in (Required, NotRequired):
|
||||
args = get_args(typ)
|
||||
if args:
|
||||
_collect_from_type(args[0], allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if origin is Literal:
|
||||
return
|
||||
|
||||
if origin in (list, set, tuple, dict, deque, frozenset):
|
||||
for arg in get_args(typ):
|
||||
_collect_from_type(arg, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if hasattr(typ, "__supertype__"):
|
||||
_collect_from_type(typ.__supertype__, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if is_typeddict(typ):
|
||||
for field_type in _safe_get_type_hints(typ).values():
|
||||
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if _is_pydantic_model(typ):
|
||||
allowlist.add((typ.__module__, typ.__name__))
|
||||
field_types = _safe_get_type_hints(typ)
|
||||
if field_types:
|
||||
for field_type in field_types.values():
|
||||
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
||||
else:
|
||||
for field_type in _pydantic_field_types(typ):
|
||||
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if dataclasses.is_dataclass(typ):
|
||||
if typ_name := getattr(typ, "__name__", None):
|
||||
allowlist.add((typ.__module__, typ_name))
|
||||
field_types = _safe_get_type_hints(typ)
|
||||
if field_types:
|
||||
for field_type in field_types.values():
|
||||
_collect_from_type(field_type, allowlist, seen, seen_ids)
|
||||
else:
|
||||
for field in dataclasses.fields(typ):
|
||||
_collect_from_type(field.type, allowlist, seen, seen_ids)
|
||||
return
|
||||
|
||||
if isinstance(typ, type) and issubclass(typ, Enum):
|
||||
allowlist.add((typ.__module__, typ.__name__))
|
||||
return
|
||||
|
||||
|
||||
def _already_seen(typ: Any, seen: set[Any], seen_ids: set[int]) -> bool:
|
||||
try:
|
||||
if typ in seen:
|
||||
return True
|
||||
seen.add(typ)
|
||||
return False
|
||||
except TypeError:
|
||||
typ_id = id(typ)
|
||||
if typ_id in seen_ids:
|
||||
return True
|
||||
seen_ids.add(typ_id)
|
||||
return False
|
||||
|
||||
|
||||
def _safe_get_type_hints(typ: Any) -> dict[str, Any]:
|
||||
try:
|
||||
module = sys.modules.get(getattr(typ, "__module__", ""))
|
||||
globalns = module.__dict__ if module else None
|
||||
localns = dict(vars(typ)) if hasattr(typ, "__dict__") else None
|
||||
return get_type_hints(
|
||||
typ, globalns=globalns, localns=localns, include_extras=True
|
||||
)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _is_pydantic_model(typ: Any) -> bool:
|
||||
if not isinstance(typ, type):
|
||||
return False
|
||||
if issubclass(typ, BaseModel):
|
||||
return True
|
||||
try:
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
except Exception:
|
||||
return False
|
||||
return issubclass(typ, BaseModelV1)
|
||||
|
||||
|
||||
def _pydantic_field_types(typ: type[Any]) -> list[Any]:
|
||||
if hasattr(typ, "model_fields"):
|
||||
return [
|
||||
field.annotation
|
||||
for field in typ.model_fields.values()
|
||||
if getattr(field, "annotation", None) is not None
|
||||
]
|
||||
if hasattr(typ, "__fields__"):
|
||||
return [
|
||||
field.outer_type_
|
||||
for field in typ.__fields__.values()
|
||||
if getattr(field, "outer_type_", None) is not None
|
||||
]
|
||||
return []
|
||||
@@ -20,6 +20,7 @@ from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from langgraph._internal import _serde
|
||||
from langgraph._internal._constants import CACHE_NS_WRITES, PREVIOUS
|
||||
from langgraph._internal._typing import MISSING, DeprecatedKwargs
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
@@ -528,7 +529,7 @@ class entrypoint(Generic[ContextT]):
|
||||
else:
|
||||
output_type = save_type = sig.return_annotation
|
||||
|
||||
return Pregel(
|
||||
graph: Pregel[Any, ContextT, Any, Any] = Pregel(
|
||||
nodes={
|
||||
func.__name__: PregelNode(
|
||||
bound=bound,
|
||||
@@ -559,5 +560,16 @@ class entrypoint(Generic[ContextT]):
|
||||
cache=self.cache,
|
||||
cache_policy=self.cache_policy,
|
||||
retry_policy=self.retry_policy or (),
|
||||
context_schema=self.context_schema, # type: ignore[arg-type]
|
||||
context_schema=self.context_schema,
|
||||
)
|
||||
if _serde.STRICT_MSGPACK_ENABLED:
|
||||
serde_allowlist = _serde.build_serde_allowlist(
|
||||
schemas=[input_type, output_type, save_type]
|
||||
+ ([self.context_schema] if self.context_schema is not None else []),
|
||||
channels=graph.channels,
|
||||
)
|
||||
graph._serde_allowlist = serde_allowlist
|
||||
graph.checkpointer = _serde.apply_checkpointer_allowlist(
|
||||
graph.checkpointer, serde_allowlist
|
||||
)
|
||||
return graph
|
||||
|
||||
@@ -29,6 +29,7 @@ from langgraph.store.base import BaseStore
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import NotRequired, Required, Self, Unpack, is_typeddict
|
||||
|
||||
from langgraph._internal import _serde
|
||||
from langgraph._internal._constants import (
|
||||
INTERRUPT,
|
||||
NS_END,
|
||||
@@ -1079,6 +1080,28 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
CompiledStateGraph: The compiled `StateGraph`.
|
||||
"""
|
||||
checkpointer = ensure_valid_checkpointer(checkpointer)
|
||||
serde_allowlist: set[tuple[str, ...]] | None = None
|
||||
if _serde.STRICT_MSGPACK_ENABLED:
|
||||
schema_types: list[type[Any]] = [
|
||||
self.state_schema,
|
||||
self.input_schema,
|
||||
self.output_schema,
|
||||
]
|
||||
if self.context_schema is not None:
|
||||
schema_types.append(self.context_schema)
|
||||
for node in self.nodes.values():
|
||||
schema_types.append(node.input_schema)
|
||||
for branches in self.branches.values():
|
||||
for branch in branches.values():
|
||||
if branch.input_schema is not None:
|
||||
schema_types.append(branch.input_schema)
|
||||
serde_allowlist = _serde.build_serde_allowlist(
|
||||
schemas=schema_types,
|
||||
channels=self.channels,
|
||||
)
|
||||
checkpointer = _serde.apply_checkpointer_allowlist(
|
||||
checkpointer, serde_allowlist
|
||||
)
|
||||
|
||||
# assign default values
|
||||
interrupt_before = interrupt_before or []
|
||||
@@ -1135,6 +1158,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
cache=cache,
|
||||
name=name or "LangGraph",
|
||||
)
|
||||
compiled._serde_allowlist = serde_allowlist
|
||||
|
||||
compiled.attach_node(START, None)
|
||||
for key, node in self.nodes.items():
|
||||
|
||||
@@ -23,6 +23,35 @@ logger = logging.getLogger(__name__)
|
||||
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
|
||||
|
||||
|
||||
def _checkpoint_ns_for_parent_command(ns: str) -> str:
|
||||
"""Return the checkpoint namespace for the parent graph.
|
||||
|
||||
The checkpoint namespace is a `|`-separated path. Each segment is usually
|
||||
of the form `name:task_id` (e.g. `parent_first:<uuid>|node:<uuid>`), but the
|
||||
runtime may also insert a purely-numeric segment (e.g. `|1`) to disambiguate
|
||||
concurrent tasks (e.g. `parent_first:<uuid>|1|node:<uuid>`).
|
||||
|
||||
Numeric segments are not real path levels, so we drop them before computing
|
||||
the parent namespace.
|
||||
"""
|
||||
|
||||
parts = ns.split(NS_SEP)
|
||||
|
||||
# Drop any trailing numeric selectors for the current frame (e.g. `...|node:<id>|1`).
|
||||
while parts and parts[-1].isdigit():
|
||||
parts.pop()
|
||||
|
||||
# Drop the current frame segment itself (e.g. the `node:<id>`).
|
||||
if parts:
|
||||
parts.pop()
|
||||
|
||||
# Drop any trailing numeric selectors for the parent frame (e.g. `...|1|node:<id>`).
|
||||
while parts and parts[-1].isdigit():
|
||||
parts.pop()
|
||||
|
||||
return NS_SEP.join(parts)
|
||||
|
||||
|
||||
def run_with_retry(
|
||||
task: PregelExecutableTask,
|
||||
retry_policy: Sequence[RetryPolicy] | None,
|
||||
@@ -50,12 +79,8 @@ def run_with_retry(
|
||||
w.invoke(cmd, config)
|
||||
break
|
||||
elif cmd.graph == Command.PARENT:
|
||||
# this command is for the parent graph, assign it to the parent
|
||||
parts = ns.split(NS_SEP)
|
||||
if parts[-1].isdigit():
|
||||
parts.pop()
|
||||
parent_ns = NS_SEP.join(parts[:-1])
|
||||
exc.args = (replace(cmd, graph=parent_ns),)
|
||||
# this command is for the parent graph, assign it to the parent.
|
||||
exc.args = (replace(cmd, graph=_checkpoint_ns_for_parent_command(ns)),)
|
||||
# bubble up
|
||||
raise
|
||||
except GraphBubbleUp:
|
||||
@@ -146,12 +171,8 @@ async def arun_with_retry(
|
||||
w.invoke(cmd, config)
|
||||
break
|
||||
elif cmd.graph == Command.PARENT:
|
||||
# this command is for the parent graph, assign it to the parent
|
||||
parts = ns.split(NS_SEP)
|
||||
if parts[-1].isdigit():
|
||||
parts.pop()
|
||||
parent_ns = NS_SEP.join(parts[:-1])
|
||||
exc.args = (replace(cmd, graph=parent_ns),)
|
||||
# this command is for the parent graph, assign it to the parent.
|
||||
exc.args = (replace(cmd, graph=_checkpoint_ns_for_parent_command(ns)),)
|
||||
# bubble up
|
||||
raise
|
||||
except GraphBubbleUp:
|
||||
|
||||
@@ -728,7 +728,6 @@ async def _acall_impl(
|
||||
)
|
||||
else:
|
||||
fut.set_result(None)
|
||||
futures()[fut] = next_task # type: ignore[index]
|
||||
else:
|
||||
# schedule the next task
|
||||
fut = cast(
|
||||
|
||||
@@ -48,6 +48,7 @@ from langgraph.store.base import BaseStore
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import Self, Unpack, deprecated, is_typeddict
|
||||
|
||||
from langgraph._internal import _serde
|
||||
from langgraph._internal._config import (
|
||||
ensure_config,
|
||||
merge_configs,
|
||||
@@ -698,9 +699,17 @@ class Pregel(
|
||||
self.config = config
|
||||
self.trigger_to_nodes = trigger_to_nodes or {}
|
||||
self.name = name
|
||||
self._serde_allowlist: set[tuple[str, ...]] | None = None
|
||||
if auto_validate:
|
||||
self.validate()
|
||||
|
||||
def _apply_checkpointer_allowlist(
|
||||
self, checkpointer: BaseCheckpointSaver | None
|
||||
) -> BaseCheckpointSaver | None:
|
||||
if not _serde.STRICT_MSGPACK_ENABLED:
|
||||
return checkpointer
|
||||
return _serde.apply_checkpointer_allowlist(checkpointer, self._serde_allowlist)
|
||||
|
||||
def get_graph(
|
||||
self, config: RunnableConfig | None = None, *, xray: int | bool = False
|
||||
) -> Graph:
|
||||
@@ -1239,6 +1248,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -1281,6 +1292,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -1329,6 +1342,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = config[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -1380,6 +1395,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -1446,6 +1463,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -1890,6 +1909,8 @@ class Pregel(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if not checkpointer:
|
||||
raise ValueError("No checkpointer set")
|
||||
|
||||
@@ -2378,6 +2399,8 @@ class Pregel(
|
||||
raise RuntimeError("checkpointer=True cannot be used for root graphs.")
|
||||
else:
|
||||
checkpointer = self.checkpointer
|
||||
if isinstance(checkpointer, BaseCheckpointSaver):
|
||||
checkpointer = self._apply_checkpointer_allowlist(checkpointer)
|
||||
if checkpointer and not config.get(CONF):
|
||||
raise ValueError(
|
||||
"Checkpointer requires one or more of the following 'configurable' "
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "1.0.8"
|
||||
version = "1.0.10rc1"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -27,7 +27,7 @@ dependencies = [
|
||||
"langchain-core>=0.1",
|
||||
"langgraph-checkpoint>=2.1.0,<5.0.0",
|
||||
"langgraph-sdk>=0.3.0,<0.4.0",
|
||||
"langgraph-prebuilt>=1.0.7,<1.1.0",
|
||||
"langgraph-prebuilt>=1.0.8,<1.1.0",
|
||||
"xxhash>=3.5.0",
|
||||
"pydantic>=2.7.4",
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -5,6 +6,7 @@ import pytest
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from psycopg import AsyncConnection, Connection
|
||||
@@ -18,30 +20,60 @@ from tests.memory_assert import ( # noqa: E402
|
||||
)
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
STRICT_MSGPACK = os.getenv("LANGGRAPH_STRICT_MSGPACK", "false").lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
)
|
||||
|
||||
|
||||
def _strict_msgpack_serde() -> JsonPlusSerializer:
|
||||
return JsonPlusSerializer(allowed_msgpack_modules=None)
|
||||
|
||||
|
||||
def _apply_strict_msgpack(checkpointer) -> None:
|
||||
if not STRICT_MSGPACK:
|
||||
return
|
||||
serde = _strict_msgpack_serde()
|
||||
if hasattr(checkpointer, "serde"):
|
||||
checkpointer.serde = serde
|
||||
if hasattr(checkpointer, "saver") and hasattr(checkpointer.saver, "serde"):
|
||||
checkpointer.saver.serde = serde
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_memory():
|
||||
yield MemorySaverAssertImmutable()
|
||||
if STRICT_MSGPACK:
|
||||
yield MemorySaverAssertImmutable(serde=_strict_msgpack_serde())
|
||||
else:
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_memory_migrate_sends():
|
||||
yield MemorySaverNeedsPendingSendsMigration()
|
||||
checkpointer = MemorySaverNeedsPendingSendsMigration()
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _checkpointer_sqlite_aes():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes(
|
||||
key=b"1234567890123456"
|
||||
)
|
||||
if STRICT_MSGPACK:
|
||||
checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes(
|
||||
serde=_strict_msgpack_serde(), key=b"1234567890123456"
|
||||
)
|
||||
else:
|
||||
checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes(
|
||||
key=b"1234567890123456"
|
||||
)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@@ -57,6 +89,7 @@ def _checkpointer_postgres():
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
@@ -79,6 +112,7 @@ def _checkpointer_postgres_pipe():
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
@@ -99,6 +133,7 @@ def _checkpointer_postgres_pool():
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
@@ -109,6 +144,7 @@ def _checkpointer_postgres_pool():
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@@ -126,6 +162,7 @@ async def _checkpointer_postgres_aio():
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
@@ -152,6 +189,7 @@ async def _checkpointer_postgres_aio_pipe():
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
@@ -176,6 +214,7 @@ async def _checkpointer_postgres_aio_pool():
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
_apply_strict_msgpack(checkpointer)
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from langgraph.types import Command
|
||||
|
||||
|
||||
def test_parent_command_from_nested_subgraph() -> None:
|
||||
class ParentState(TypedDict):
|
||||
jump_from_idx: int
|
||||
|
||||
class ChildState(TypedDict):
|
||||
jump: bool
|
||||
|
||||
child_builder: StateGraph[ChildState] = StateGraph(ChildState)
|
||||
|
||||
def child_node(state: ChildState) -> Command | ChildState:
|
||||
if state["jump"]:
|
||||
return Command(graph=Command.PARENT, goto="parent_second")
|
||||
return state
|
||||
|
||||
child_builder.add_node("node", child_node)
|
||||
child_builder.add_edge(START, "node")
|
||||
|
||||
child_0 = child_builder.compile()
|
||||
child_1 = child_builder.compile()
|
||||
|
||||
parent_builder: StateGraph[ParentState] = StateGraph(ParentState)
|
||||
|
||||
def parent_first(state: ParentState) -> ParentState:
|
||||
child_0.invoke({"jump": state["jump_from_idx"] == 1})
|
||||
if state["jump_from_idx"] == 1:
|
||||
raise AssertionError("Shouldn't be here")
|
||||
|
||||
child_1.invoke({"jump": state["jump_from_idx"] == 2})
|
||||
if state["jump_from_idx"] == 2:
|
||||
raise AssertionError("Shouldn't be here")
|
||||
|
||||
return state
|
||||
|
||||
def parent_second(state: ParentState) -> ParentState:
|
||||
return state
|
||||
|
||||
parent_builder.add_node("parent_first", parent_first)
|
||||
parent_builder.add_node("parent_second", parent_second)
|
||||
parent_builder.add_edge(START, "parent_first")
|
||||
parent_builder.add_edge("parent_second", END)
|
||||
|
||||
graph = parent_builder.compile()
|
||||
|
||||
assert graph.invoke({"jump_from_idx": 1}) == {"jump_from_idx": 1}
|
||||
assert graph.invoke({"jump_from_idx": 2}) == {"jump_from_idx": 2}
|
||||
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from langgraph.types import Command
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def test_parent_command_from_nested_subgraph() -> None:
|
||||
class ParentState(TypedDict):
|
||||
jump_from_idx: int
|
||||
|
||||
class ChildState(TypedDict):
|
||||
jump: bool
|
||||
|
||||
child_builder: StateGraph[ChildState] = StateGraph(ChildState)
|
||||
|
||||
async def child_node(state: ChildState) -> Command | ChildState:
|
||||
if state["jump"]:
|
||||
return Command(graph=Command.PARENT, goto="parent_second")
|
||||
return state
|
||||
|
||||
child_builder.add_node("node", child_node)
|
||||
child_builder.add_edge(START, "node")
|
||||
|
||||
child_0 = child_builder.compile()
|
||||
child_1 = child_builder.compile()
|
||||
|
||||
parent_builder: StateGraph[ParentState] = StateGraph(ParentState)
|
||||
|
||||
async def parent_first(state: ParentState, config: RunnableConfig) -> ParentState:
|
||||
await child_0.ainvoke({"jump": state["jump_from_idx"] == 1}, config)
|
||||
if state["jump_from_idx"] == 1:
|
||||
raise AssertionError("Shouldn't be here")
|
||||
|
||||
await child_1.ainvoke({"jump": state["jump_from_idx"] == 2}, config)
|
||||
if state["jump_from_idx"] == 2:
|
||||
raise AssertionError("Shouldn't be here")
|
||||
|
||||
return state
|
||||
|
||||
async def parent_second(state: ParentState) -> ParentState:
|
||||
return state
|
||||
|
||||
parent_builder.add_node("parent_first", parent_first)
|
||||
parent_builder.add_node("parent_second", parent_second)
|
||||
parent_builder.add_edge(START, "parent_first")
|
||||
parent_builder.add_edge("parent_second", END)
|
||||
|
||||
graph = parent_builder.compile().with_config(recursion_limit=10)
|
||||
|
||||
assert await graph.ainvoke({"jump_from_idx": 1}) == {"jump_from_idx": 1}
|
||||
assert await graph.ainvoke({"jump_from_idx": 2}) == {"jump_from_idx": 2}
|
||||
@@ -5800,6 +5800,284 @@ def test_multiple_interrupts_functional_cache(
|
||||
assert counter == 6
|
||||
|
||||
|
||||
def test_task_before_interrupt_resume(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly when a @task runs
|
||||
before interrupt-producing tasks in an @entrypoint.
|
||||
|
||||
The @task wrapper on both setup and ask is essential to reproduce the bug:
|
||||
- @task on setup triggers a mid-step put_writes (creating a new pending_writes list)
|
||||
- @task on ask means interrupt() runs in a child scratchpad that must
|
||||
delegate to the parent for null resume consumption tracking
|
||||
"""
|
||||
|
||||
@entrypoint(checkpointer=sync_checkpointer)
|
||||
def workflow(number_of_topics: int) -> dict:
|
||||
@task
|
||||
def setup() -> int:
|
||||
return number_of_topics
|
||||
|
||||
@task
|
||||
def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
n = setup().result()
|
||||
|
||||
answers = []
|
||||
for i in range(n):
|
||||
q = f"Whats the answer for topic {i + 1}?"
|
||||
answers.append(ask(q).result())
|
||||
|
||||
return {"answers": answers}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - should get first interrupt
|
||||
result = workflow.invoke(2, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 1?"
|
||||
|
||||
# Resume with answer for topic 1 - should get second interrupt
|
||||
result = workflow.invoke(Command(resume="answer1"), config=config)
|
||||
assert "__interrupt__" in result, f"Expected interrupt for topic 2, got: {result}"
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 2?"
|
||||
|
||||
# Resume with answer for topic 2 - should get final result
|
||||
result = workflow.invoke(Command(resume="answer2"), config=config)
|
||||
assert result == {"answers": ["answer1", "answer2"]}
|
||||
|
||||
|
||||
def test_multiple_tasks_before_interrupt_resume(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly when multiple @tasks
|
||||
run before an interrupt-producing task in an @entrypoint."""
|
||||
|
||||
@entrypoint(checkpointer=sync_checkpointer)
|
||||
def workflow(inputs: dict) -> dict:
|
||||
@task
|
||||
def step_a(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
@task
|
||||
def step_b(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
@task
|
||||
def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
a = step_a(inputs["x"]).result()
|
||||
b = step_b(a).result()
|
||||
|
||||
answer = ask(f"Result so far is {b}. What next?").result()
|
||||
|
||||
return {"computed": b, "answer": answer}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - should get interrupt
|
||||
result = workflow.invoke({"x": 5}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Result so far is 12. What next?"
|
||||
|
||||
# Resume
|
||||
result = workflow.invoke(Command(resume="continue"), config=config)
|
||||
assert result == {"computed": 12, "answer": "continue"}
|
||||
|
||||
|
||||
def test_no_redundant_put_writes_for_cached_task(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Cached @tasks on resume must not trigger redundant put_writes."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from langgraph.pregel._loop import PregelLoop
|
||||
|
||||
@task
|
||||
def setup(x: int) -> int:
|
||||
return x
|
||||
|
||||
@task
|
||||
def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
@entrypoint(checkpointer=sync_checkpointer)
|
||||
def workflow(x: int) -> dict:
|
||||
n = setup(x).result()
|
||||
answer = ask(f"q{n}").result()
|
||||
return {"answer": answer}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
result = workflow.invoke(1, config=config)
|
||||
assert "__interrupt__" in result
|
||||
|
||||
put_writes_task_ids: list[str] = []
|
||||
orig = PregelLoop.put_writes
|
||||
|
||||
def spy(self, task_id, writes):
|
||||
put_writes_task_ids.append(task_id)
|
||||
return orig(self, task_id, writes)
|
||||
|
||||
with patch.object(PregelLoop, "put_writes", spy):
|
||||
result = workflow.invoke(Command(resume="ans"), config=config)
|
||||
|
||||
assert result == {"answer": "ans"}
|
||||
# Count unique non-null task IDs that got put_writes.
|
||||
# Should be exactly 2: the ask task and the entrypoint task.
|
||||
# If 3, the cached setup task is being redundantly re-committed.
|
||||
non_null = set(tid for tid in put_writes_task_ids if not tid.startswith("00000000"))
|
||||
assert len(non_null) == 2, (
|
||||
f"Expected 2 task IDs in put_writes (ask + entrypoint), got {len(non_null)}"
|
||||
)
|
||||
|
||||
|
||||
def test_node_before_interrupt_resume_graph_api(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly in a StateGraph when a
|
||||
node runs before a node that calls interrupt(). This is the graph-API
|
||||
analog of test_task_before_interrupt_resume (entrypoint API)."""
|
||||
|
||||
class State(TypedDict):
|
||||
topics: list[str]
|
||||
answers: Annotated[list[str], operator.add]
|
||||
|
||||
def setup(state: State) -> dict:
|
||||
return {"topics": [f"topic {i + 1}" for i in range(len(state["topics"]))]}
|
||||
|
||||
def ask(state: State) -> dict:
|
||||
answers = []
|
||||
for topic in state["topics"]:
|
||||
answer = interrupt(f"Whats the answer for {topic}?")
|
||||
answers.append(answer)
|
||||
return {"answers": answers}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("setup", setup)
|
||||
.add_node("ask", ask)
|
||||
.add_edge(START, "setup")
|
||||
.add_edge("setup", "ask")
|
||||
.add_edge("ask", END)
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - setup runs, then ask interrupts on the first topic
|
||||
result = graph.invoke({"topics": ["a", "b"], "answers": []}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 1?"
|
||||
|
||||
# Resume with answer for topic 1 - should get second interrupt
|
||||
result = graph.invoke(Command(resume="answer1"), config=config)
|
||||
assert "__interrupt__" in result, f"Expected interrupt for topic 2, got: {result}"
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 2?"
|
||||
|
||||
# Resume with answer for topic 2 - should complete
|
||||
result = graph.invoke(Command(resume="answer2"), config=config)
|
||||
assert result == {
|
||||
"topics": ["topic 1", "topic 2"],
|
||||
"answers": ["answer1", "answer2"],
|
||||
}
|
||||
|
||||
|
||||
def test_multiple_nodes_before_interrupt_resume_graph_api(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly in a StateGraph when
|
||||
multiple nodes run before a node that calls interrupt(). This is the
|
||||
graph-API analog of test_multiple_tasks_before_interrupt_resume."""
|
||||
|
||||
class State(TypedDict):
|
||||
value: int
|
||||
answer: str
|
||||
|
||||
def step_a(state: State) -> dict:
|
||||
return {"value": state["value"] + 1}
|
||||
|
||||
def step_b(state: State) -> dict:
|
||||
return {"value": state["value"] * 2}
|
||||
|
||||
def ask(state: State) -> dict:
|
||||
answer = interrupt(f"Result so far is {state['value']}. What next?")
|
||||
return {"answer": answer}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("step_a", step_a)
|
||||
.add_node("step_b", step_b)
|
||||
.add_node("ask", ask)
|
||||
.add_edge(START, "step_a")
|
||||
.add_edge("step_a", "step_b")
|
||||
.add_edge("step_b", "ask")
|
||||
.add_edge("ask", END)
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - step_a and step_b run, then ask interrupts
|
||||
result = graph.invoke({"value": 5, "answer": ""}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Result so far is 12. What next?"
|
||||
|
||||
# Resume - should complete
|
||||
result = graph.invoke(Command(resume="continue"), config=config)
|
||||
assert result == {"value": 12, "answer": "continue"}
|
||||
|
||||
|
||||
def test_node_before_multiple_interrupt_cycles_graph_api(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that a node running before an interrupt node does not interfere
|
||||
with multiple interrupt/resume cycles in a StateGraph."""
|
||||
|
||||
class State(TypedDict):
|
||||
count: int
|
||||
data: str
|
||||
|
||||
def prepare(state: State) -> dict:
|
||||
return {"count": state["count"] + 10}
|
||||
|
||||
def multi_interrupt(state: State) -> dict:
|
||||
first = interrupt("First question?")
|
||||
second = interrupt("Second question?")
|
||||
return {"data": f"{first},{second}"}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("prepare", prepare)
|
||||
.add_node("multi_interrupt", multi_interrupt)
|
||||
.add_edge(START, "prepare")
|
||||
.add_edge("prepare", "multi_interrupt")
|
||||
.add_edge("multi_interrupt", END)
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - prepare runs, multi_interrupt hits first interrupt
|
||||
result = graph.invoke({"count": 0, "data": ""}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "First question?"
|
||||
|
||||
# Resume first interrupt - hits second interrupt
|
||||
result = graph.invoke(Command(resume="first_answer"), config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Second question?"
|
||||
|
||||
# Resume second interrupt - completes
|
||||
result = graph.invoke(Command(resume="second_answer"), config=config)
|
||||
assert result == {"count": 10, "data": "first_answer,second_answer"}
|
||||
|
||||
|
||||
def test_double_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> None:
|
||||
class AgentState(TypedDict):
|
||||
input: str
|
||||
|
||||
@@ -7920,6 +7920,290 @@ async def test_interrupts_in_tasks_surfaced_once(
|
||||
assert result[1] == "Added Will!"
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_task_before_interrupt_resume(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly when a @task runs
|
||||
before interrupt-producing tasks in an @entrypoint.
|
||||
|
||||
The @task wrapper on both setup and ask is essential to reproduce the bug:
|
||||
- @task on setup triggers a mid-step put_writes (creating a new pending_writes list)
|
||||
- @task on ask means interrupt() runs in a child scratchpad that must
|
||||
delegate to the parent for null resume consumption tracking
|
||||
"""
|
||||
|
||||
@entrypoint(checkpointer=async_checkpointer)
|
||||
async def workflow(number_of_topics: int) -> dict:
|
||||
@task
|
||||
async def setup() -> int:
|
||||
return number_of_topics
|
||||
|
||||
@task
|
||||
async def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
n = await setup()
|
||||
|
||||
answers = []
|
||||
for i in range(n):
|
||||
q = f"Whats the answer for topic {i + 1}?"
|
||||
answers.append(await ask(q))
|
||||
|
||||
return {"answers": answers}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - should get first interrupt
|
||||
result = await workflow.ainvoke(2, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 1?"
|
||||
|
||||
# Resume with answer for topic 1 - should get second interrupt
|
||||
result = await workflow.ainvoke(Command(resume="answer1"), config=config)
|
||||
assert "__interrupt__" in result, f"Expected interrupt for topic 2, got: {result}"
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 2?"
|
||||
|
||||
# Resume with answer for topic 2 - should get final result
|
||||
result = await workflow.ainvoke(Command(resume="answer2"), config=config)
|
||||
assert result == {"answers": ["answer1", "answer2"]}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_multiple_tasks_before_interrupt_resume(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly when multiple @tasks
|
||||
run before an interrupt-producing task in an @entrypoint."""
|
||||
|
||||
@entrypoint(checkpointer=async_checkpointer)
|
||||
async def workflow(inputs: dict) -> dict:
|
||||
@task
|
||||
async def step_a(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
@task
|
||||
async def step_b(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
@task
|
||||
async def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
a = await step_a(inputs["x"])
|
||||
b = await step_b(a)
|
||||
|
||||
answer = await ask(f"Result so far is {b}. What next?")
|
||||
|
||||
return {"computed": b, "answer": answer}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - should get interrupt
|
||||
result = await workflow.ainvoke({"x": 5}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Result so far is 12. What next?"
|
||||
|
||||
# Resume
|
||||
result = await workflow.ainvoke(Command(resume="continue"), config=config)
|
||||
assert result == {"computed": 12, "answer": "continue"}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_no_redundant_put_writes_for_cached_task(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Cached @tasks on resume must not trigger redundant put_writes."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from langgraph.pregel._loop import PregelLoop
|
||||
|
||||
@task
|
||||
async def setup(x: int) -> int:
|
||||
return x
|
||||
|
||||
@task
|
||||
async def ask(question: str) -> str:
|
||||
return interrupt(question)
|
||||
|
||||
@entrypoint(checkpointer=async_checkpointer)
|
||||
async def workflow(x: int) -> dict:
|
||||
n = await setup(x)
|
||||
answer = await ask(f"q{n}")
|
||||
return {"answer": answer}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
result = await workflow.ainvoke(1, config=config)
|
||||
assert "__interrupt__" in result
|
||||
|
||||
put_writes_task_ids: list[str] = []
|
||||
orig = PregelLoop.put_writes
|
||||
|
||||
def spy(self, task_id, writes):
|
||||
put_writes_task_ids.append(task_id)
|
||||
return orig(self, task_id, writes)
|
||||
|
||||
with patch.object(PregelLoop, "put_writes", spy):
|
||||
result = await workflow.ainvoke(Command(resume="ans"), config=config)
|
||||
|
||||
assert result == {"answer": "ans"}
|
||||
# Count unique non-null task IDs that got put_writes.
|
||||
# Should be exactly 2: the ask task and the entrypoint task.
|
||||
# If 3, the cached setup task is being redundantly re-committed.
|
||||
non_null = set(tid for tid in put_writes_task_ids if not tid.startswith("00000000"))
|
||||
assert len(non_null) == 2, (
|
||||
f"Expected 2 task IDs in put_writes (ask + entrypoint), got {len(non_null)}"
|
||||
)
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_node_before_interrupt_resume_graph_api(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly in a StateGraph when a
|
||||
node runs before a node that calls interrupt(). This is the graph-API
|
||||
analog of test_task_before_interrupt_resume (entrypoint API)."""
|
||||
|
||||
class State(TypedDict):
|
||||
topics: list[str]
|
||||
answers: Annotated[list[str], operator.add]
|
||||
|
||||
def setup(state: State) -> dict:
|
||||
return {"topics": [f"topic {i + 1}" for i in range(len(state["topics"]))]}
|
||||
|
||||
def ask(state: State) -> dict:
|
||||
answers = []
|
||||
for topic in state["topics"]:
|
||||
answer = interrupt(f"Whats the answer for {topic}?")
|
||||
answers.append(answer)
|
||||
return {"answers": answers}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("setup", setup)
|
||||
.add_node("ask", ask)
|
||||
.add_edge(START, "setup")
|
||||
.add_edge("setup", "ask")
|
||||
.add_edge("ask", END)
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - setup runs, then ask interrupts on the first topic
|
||||
result = await graph.ainvoke({"topics": ["a", "b"], "answers": []}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 1?"
|
||||
|
||||
# Resume with answer for topic 1 - should get second interrupt
|
||||
result = await graph.ainvoke(Command(resume="answer1"), config=config)
|
||||
assert "__interrupt__" in result, f"Expected interrupt for topic 2, got: {result}"
|
||||
assert len(result["__interrupt__"]) == 1
|
||||
assert result["__interrupt__"][0].value == "Whats the answer for topic 2?"
|
||||
|
||||
# Resume with answer for topic 2 - should complete
|
||||
result = await graph.ainvoke(Command(resume="answer2"), config=config)
|
||||
assert result == {
|
||||
"topics": ["topic 1", "topic 2"],
|
||||
"answers": ["answer1", "answer2"],
|
||||
}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_multiple_nodes_before_interrupt_resume_graph_api(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that Command(resume=value) works correctly in a StateGraph when
|
||||
multiple nodes run before a node that calls interrupt(). This is the
|
||||
graph-API analog of test_multiple_tasks_before_interrupt_resume."""
|
||||
|
||||
class State(TypedDict):
|
||||
value: int
|
||||
answer: str
|
||||
|
||||
def step_a(state: State) -> dict:
|
||||
return {"value": state["value"] + 1}
|
||||
|
||||
def step_b(state: State) -> dict:
|
||||
return {"value": state["value"] * 2}
|
||||
|
||||
def ask(state: State) -> dict:
|
||||
answer = interrupt(f"Result so far is {state['value']}. What next?")
|
||||
return {"answer": answer}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("step_a", step_a)
|
||||
.add_node("step_b", step_b)
|
||||
.add_node("ask", ask)
|
||||
.add_edge(START, "step_a")
|
||||
.add_edge("step_a", "step_b")
|
||||
.add_edge("step_b", "ask")
|
||||
.add_edge("ask", END)
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - step_a and step_b run, then ask interrupts
|
||||
result = await graph.ainvoke({"value": 5, "answer": ""}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Result so far is 12. What next?"
|
||||
|
||||
# Resume - should complete
|
||||
result = await graph.ainvoke(Command(resume="continue"), config=config)
|
||||
assert result == {"value": 12, "answer": "continue"}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_node_before_multiple_interrupt_cycles_graph_api(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that a node running before an interrupt node does not interfere
|
||||
with multiple interrupt/resume cycles in a StateGraph."""
|
||||
|
||||
class State(TypedDict):
|
||||
count: int
|
||||
data: str
|
||||
|
||||
def prepare(state: State) -> dict:
|
||||
return {"count": state["count"] + 10}
|
||||
|
||||
def multi_interrupt(state: State) -> dict:
|
||||
first = interrupt("First question?")
|
||||
second = interrupt("Second question?")
|
||||
return {"data": f"{first},{second}"}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("prepare", prepare)
|
||||
.add_node("multi_interrupt", multi_interrupt)
|
||||
.add_edge(START, "prepare")
|
||||
.add_edge("prepare", "multi_interrupt")
|
||||
.add_edge("multi_interrupt", END)
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# First invocation - prepare runs, multi_interrupt hits first interrupt
|
||||
result = await graph.ainvoke({"count": 0, "data": ""}, config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "First question?"
|
||||
|
||||
# Resume first interrupt - hits second interrupt
|
||||
result = await graph.ainvoke(Command(resume="first_answer"), config=config)
|
||||
assert "__interrupt__" in result
|
||||
assert result["__interrupt__"][0].value == "Second question?"
|
||||
|
||||
# Resume second interrupt - completes
|
||||
result = await graph.ainvoke(Command(resume="second_answer"), config=config)
|
||||
assert result == {"count": 10, "data": "first_answer,second_answer"}
|
||||
|
||||
|
||||
async def test_pregel_loop_refcount():
|
||||
gc.collect()
|
||||
try:
|
||||
|
||||
@@ -8,6 +8,7 @@ import uuid
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, Optional
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ByteSize,
|
||||
@@ -23,7 +24,10 @@ from pydantic import (
|
||||
|
||||
from langgraph._internal._pydantic import is_supported_by_pydantic
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.types import Command, Interrupt, interrupt
|
||||
from tests.any_str import AnyStr
|
||||
|
||||
|
||||
def test_is_supported_by_pydantic() -> None:
|
||||
@@ -312,3 +316,47 @@ def test_pydantic_state_field_validator():
|
||||
g = builder.compile()
|
||||
res = g.invoke(input_state)
|
||||
assert res["text"] == "Hello, Validated John!"
|
||||
|
||||
|
||||
class FunctionalState(BaseModel):
|
||||
a: str
|
||||
b: str | None = None
|
||||
|
||||
|
||||
def test_interrupt_functional_pydantic(sync_checkpointer: BaseCheckpointSaver) -> None:
|
||||
called_count = 0
|
||||
|
||||
@task
|
||||
def foo(state: FunctionalState) -> FunctionalState:
|
||||
nonlocal called_count
|
||||
called_count += 1
|
||||
return FunctionalState(**{"a": state.a + "foo"})
|
||||
|
||||
@task
|
||||
def bar(state: FunctionalState) -> dict:
|
||||
return {"a": state.a + "bar", "b": state.b}
|
||||
|
||||
@entrypoint(checkpointer=sync_checkpointer)
|
||||
def graph(inputs: FunctionalState) -> FunctionalState:
|
||||
fut_foo = foo(inputs)
|
||||
value = interrupt("Provide value for bar:")
|
||||
foo_res = fut_foo.result()
|
||||
assert isinstance(foo_res, FunctionalState)
|
||||
bar_input = FunctionalState(a=foo_res.a, b=value)
|
||||
fut_bar = bar(bar_input)
|
||||
return fut_bar.result()
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
assert graph.invoke(FunctionalState(a=""), config) == {
|
||||
"__interrupt__": [
|
||||
Interrupt(
|
||||
value="Provide value for bar:",
|
||||
id=AnyStr(),
|
||||
)
|
||||
]
|
||||
}
|
||||
# Resume with an answer
|
||||
res = graph.invoke(Command(resume="bar"), config)
|
||||
assert res == {"a": "foobar", "b": "bar"}
|
||||
assert called_count == 1
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.pregel._retry import _should_retry_on
|
||||
from langgraph.pregel._retry import _checkpoint_ns_for_parent_command, _should_retry_on
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
|
||||
@@ -78,6 +78,22 @@ def test_should_retry_on_empty_sequence():
|
||||
assert _should_retry_on(policy, ValueError("test error")) is False
|
||||
|
||||
|
||||
def test_checkpoint_ns_for_parent_command() -> None:
|
||||
assert _checkpoint_ns_for_parent_command("") == ""
|
||||
assert _checkpoint_ns_for_parent_command("node:1") == ""
|
||||
assert _checkpoint_ns_for_parent_command("node:1|child:2") == "node:1"
|
||||
assert _checkpoint_ns_for_parent_command("node:1|1|child:2") == "node:1"
|
||||
assert _checkpoint_ns_for_parent_command("node:1|1|child:2|1") == "node:1"
|
||||
assert (
|
||||
_checkpoint_ns_for_parent_command("parent:1|1|child:1|1|node:1|1")
|
||||
== "parent:1|1|child:1"
|
||||
)
|
||||
assert (
|
||||
_checkpoint_ns_for_parent_command("parent:1|1|child:1|1|node:1")
|
||||
== "parent:1|1|child:1"
|
||||
)
|
||||
|
||||
|
||||
def test_should_retry_default_retry_on():
|
||||
"""Test the default retry_on function."""
|
||||
import httpx
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType, Optional, Union
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import NotRequired, Required, TypedDict
|
||||
|
||||
from langgraph._internal._serde import (
|
||||
collect_allowlist_from_schemas,
|
||||
curated_core_allowlist,
|
||||
)
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
RED = "red"
|
||||
BLUE = "blue"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InnerDataclass:
|
||||
value: int
|
||||
|
||||
|
||||
class InnerModel(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
value: int
|
||||
child: Node | None = None
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class MissingType:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MissingRefDataclass:
|
||||
payload: MissingType
|
||||
|
||||
|
||||
class Payload(TypedDict):
|
||||
item: InnerDataclass
|
||||
maybe: NotRequired[InnerModel]
|
||||
required: Required[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class NestedDataclass:
|
||||
inner: InnerDataclass
|
||||
items: list[InnerModel]
|
||||
mapping: dict[str, InnerDataclass]
|
||||
optional: InnerModel | None
|
||||
union: InnerDataclass | InnerModel
|
||||
queue: deque[InnerDataclass]
|
||||
frozen: frozenset[InnerModel]
|
||||
|
||||
|
||||
AnnotatedList = Annotated[list[InnerDataclass], "meta"]
|
||||
UserId = NewType("UserId", int)
|
||||
|
||||
|
||||
class DummyChannel:
|
||||
@property
|
||||
def ValueType(self) -> type[InnerDataclass]:
|
||||
return InnerDataclass
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> type[InnerModel]:
|
||||
return InnerModel
|
||||
|
||||
|
||||
def test_curated_core_allowlist_includes_messages() -> None:
|
||||
try:
|
||||
from langchain_core.messages import BaseMessage
|
||||
except Exception:
|
||||
pytest.skip("langchain_core not available")
|
||||
allowlist = curated_core_allowlist()
|
||||
assert (BaseMessage.__module__, BaseMessage.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_basic_models() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(
|
||||
schemas=[InnerDataclass, InnerModel, Color]
|
||||
)
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
assert (Color.__module__, Color.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_nested_containers() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[NestedDataclass])
|
||||
assert (NestedDataclass.__module__, NestedDataclass.__name__) in allowlist
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_annotated_and_union() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(
|
||||
schemas=[AnnotatedList, InnerModel | None, InnerDataclass | None]
|
||||
)
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_literal_and_any() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[Any, Literal["a"]])
|
||||
assert allowlist == set()
|
||||
|
||||
|
||||
def test_collect_allowlist_typeddict_fields_only() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[Payload])
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
assert (Payload.__module__, Payload.__name__) not in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_forward_refs() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[Node])
|
||||
assert (Node.__module__, Node.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_missing_forward_ref() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[MissingRefDataclass])
|
||||
assert allowlist == {(MissingRefDataclass.__module__, MissingRefDataclass.__name__)}
|
||||
|
||||
|
||||
def test_collect_allowlist_newtype_supertype() -> None:
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[UserId])
|
||||
assert allowlist == set()
|
||||
|
||||
|
||||
def test_collect_allowlist_channels() -> None:
|
||||
channels = {"a": DummyChannel(), "b": DummyChannel()}
|
||||
allowlist = collect_allowlist_from_schemas(channels=channels)
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_pep604_union() -> None:
|
||||
schema = InnerDataclass | InnerModel
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[schema])
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
|
||||
|
||||
def test_collect_allowlist_typing_union_optional() -> None:
|
||||
typing_optional = Optional[InnerDataclass] # noqa: UP045
|
||||
typing_union = Union[InnerDataclass, InnerModel] # noqa: UP007
|
||||
allowlist = collect_allowlist_from_schemas(schemas=[typing_optional, typing_union])
|
||||
assert (InnerDataclass.__module__, InnerDataclass.__name__) in allowlist
|
||||
assert (InnerModel.__module__, InnerModel.__name__) in allowlist
|
||||
@@ -0,0 +1,641 @@
|
||||
"""Tests for subgraph persistence behavior (sync).
|
||||
|
||||
Covers three checkpointer settings for subgraph state:
|
||||
- checkpointer=False: no persistence, even when parent has a checkpointer
|
||||
- checkpointer=None (default): "stateless" — inherits parent checkpointer for
|
||||
interrupt support, but state resets each invocation. This is the common case
|
||||
when an agent is invoked from inside a tool used by another agent.
|
||||
- checkpointer=True: "stateful" — state accumulates across invocations on the same thread id
|
||||
"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.graph.message import MessagesState
|
||||
from langgraph.types import Command, Interrupt, interrupt
|
||||
from tests.any_str import AnyStr
|
||||
|
||||
|
||||
class ParentState(TypedDict):
|
||||
result: str
|
||||
|
||||
|
||||
# -- checkpointer=None (stateless) --
|
||||
|
||||
|
||||
def test_stateless_interrupt_resume(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None (the default) can
|
||||
still support interrupt/resume when invoked from inside a parent graph that
|
||||
has a checkpointer. This is the "stateless" pattern — the subgraph inherits
|
||||
the parent's checkpointer just enough to pause and resume, but does not
|
||||
retain any state across separate parent invocations. This pattern commonly
|
||||
appears when an agent is invoked from inside a tool used by another agent.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing.
|
||||
# Two nodes: "process" interrupts then echoes, "respond" returns "Done".
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
resp = inner.invoke({"messages": [HumanMessage(content="apples")]})
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke hits the interrupt
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume completes the subgraph
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
|
||||
def test_stateless_state_resets(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None (the default) does
|
||||
not retain any message history between separate parent invocations. Each time
|
||||
the parent graph invokes the subgraph, it starts with a clean slate. This
|
||||
confirms the "stateless" behavior: even though the parent has a checkpointer,
|
||||
the subgraph state is not persisted across calls.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph: echoes "Processing: <input>"
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = inner.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = parent.invoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processing: tell me about apples"}
|
||||
|
||||
result2 = parent.invoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processing: tell me about bananas"}
|
||||
|
||||
# Both invocations produce fresh history — no memory of prior call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
def test_stateless_state_resets_with_interrupt(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None resets its state
|
||||
between parent invocations even when interrupt/resume is used. The subgraph
|
||||
is invoked twice from the parent, each time with an interrupt that must be
|
||||
resumed. After both invoke+resume cycles, each subgraph run should only
|
||||
contain its own messages — no bleed-over from the previous run.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = inner.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke+resume cycle
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Second invoke+resume cycle
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Both invocations produce fresh history — no memory of prior call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
# -- checkpointer=False --
|
||||
|
||||
|
||||
def test_checkpointer_false_no_persistence(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=False gets no
|
||||
persistence at all, even when the parent graph has a checkpointer. Unlike
|
||||
the default (checkpointer=None) which inherits just enough from the parent
|
||||
to support interrupt/resume, checkpointer=False explicitly opts out of all
|
||||
checkpoint behavior. Each invocation starts completely fresh.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph with checkpointer=False
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processed: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile(checkpointer=False)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = inner.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = parent.invoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processed: tell me about apples"}
|
||||
|
||||
result2 = parent.invoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processed: tell me about bananas"}
|
||||
|
||||
# Both start fresh — no history from first call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processed: tell me about apples",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processed: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
# -- checkpointer=True (stateful) --
|
||||
|
||||
|
||||
def test_stateful_state_accumulates(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=True ("stateful")
|
||||
retains its message history across separate parent invocations. To enable
|
||||
this, the subgraph is wrapped in an outer graph compiled with
|
||||
checkpointer=True — this wrapper gives the inner subgraph its own persistent
|
||||
checkpoint namespace. After two parent calls, the second subgraph invocation
|
||||
should see messages from both the first and second calls.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap the inner subgraph with checkpointer=True to enable stateful.
|
||||
# The wrapper graph gives the subgraph its own persistent checkpoint
|
||||
# namespace, keyed by the node name ("agent").
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = wrapper.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = parent.invoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processing: tell me about apples"}
|
||||
|
||||
result2 = parent.invoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processing: tell me about bananas"}
|
||||
|
||||
# First call: fresh history
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
]
|
||||
# Second call: retains messages from first call
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
def test_stateful_state_accumulates_with_interrupt(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a stateful subgraph (checkpointer=True) retains its
|
||||
message history across parent invocations even when interrupt/resume is
|
||||
involved. The subgraph interrupts before echoing, then responds "Done".
|
||||
After two invoke+resume cycles, the second run should contain the full
|
||||
accumulated history from both calls.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap with checkpointer=True for stateful
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = wrapper.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke+resume cycle
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Second invoke+resume cycle
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# First call: fresh history
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
# Second call: retains messages from first call
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
def test_stateful_interrupt_resume(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a stateful subgraph (checkpointer=True) correctly
|
||||
supports interrupt/resume while also accumulating state. Each invoke+resume
|
||||
pair triggers the subgraph, and after the second pair completes we verify
|
||||
both the per-step invoke outputs and the accumulated message history. This
|
||||
exercises the full lifecycle: interrupt, resume, state accumulation.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap with checkpointer=True for stateful
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = wrapper.invoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invocation: hits interrupt
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume: completes first call
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
|
||||
# Second invocation: hits interrupt, state accumulated from first call
|
||||
result = parent.invoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume: completes second call with accumulated state
|
||||
result = parent.invoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
def test_stateful_namespace_isolation(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that two different stateful subgraphs (checkpointer=True)
|
||||
maintain completely independent state when they use different wrapper node
|
||||
names. A "fruit_agent" and "veggie_agent" are each wrapped in their own
|
||||
stateful graph. After two parent invocations, each agent should only
|
||||
see its own accumulated history with no cross-contamination between them.
|
||||
"""
|
||||
|
||||
# Build two simple echo subgraphs with different prefixes
|
||||
def fruit_echo(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content=f"Fruit: {state['messages'][-1].text}")]}
|
||||
|
||||
def veggie_echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Veggie: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
fruit_inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", fruit_echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
veggie_inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", veggie_echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap each with checkpointer=True, using different node names to get
|
||||
# independent checkpoint namespaces
|
||||
fruit = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("fruit_agent", fruit_inner)
|
||||
.add_edge(START, "fruit_agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
veggie = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("veggie_agent", veggie_inner)
|
||||
.add_edge(START, "veggie_agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
fruit_msgs: list[list[str]] = []
|
||||
veggie_msgs: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
def call_both(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
suffix = "round 1" if call_count == 1 else "round 2"
|
||||
f = fruit.invoke({"messages": [HumanMessage(content=f"cherries {suffix}")]})
|
||||
v = veggie.invoke({"messages": [HumanMessage(content=f"broccoli {suffix}")]})
|
||||
fruit_msgs.append([m.text for m in f["messages"]])
|
||||
veggie_msgs.append([m.text for m in v["messages"]])
|
||||
return {"result": f["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_both", call_both)
|
||||
.add_edge(START, "call_both")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = parent.invoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Fruit: cherries round 1"}
|
||||
|
||||
result2 = parent.invoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Fruit: cherries round 2"}
|
||||
|
||||
# First call: each agent sees only its own history
|
||||
assert fruit_msgs[0] == ["cherries round 1", "Fruit: cherries round 1"]
|
||||
assert veggie_msgs[0] == ["broccoli round 1", "Veggie: broccoli round 1"]
|
||||
|
||||
# Second call: each accumulated independently — no cross-contamination
|
||||
assert fruit_msgs[1] == [
|
||||
"cherries round 1",
|
||||
"Fruit: cherries round 1",
|
||||
"cherries round 2",
|
||||
"Fruit: cherries round 2",
|
||||
]
|
||||
assert veggie_msgs[1] == [
|
||||
"broccoli round 1",
|
||||
"Veggie: broccoli round 1",
|
||||
"broccoli round 2",
|
||||
"Veggie: broccoli round 2",
|
||||
]
|
||||
@@ -0,0 +1,662 @@
|
||||
"""Tests for subgraph persistence behavior (async).
|
||||
|
||||
Covers three checkpointer settings for subgraph state:
|
||||
- checkpointer=False: no persistence, even when parent has a checkpointer
|
||||
- checkpointer=None (default): "stateless" — inherits parent checkpointer for
|
||||
interrupt support, but state resets each invocation. This is the common case
|
||||
when an agent is invoked from inside a tool used by another agent.
|
||||
- checkpointer=True: "stateful" — state accumulates across invocations on the same thread id
|
||||
"""
|
||||
|
||||
import sys
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.graph.message import MessagesState
|
||||
from langgraph.types import Command, Interrupt, interrupt
|
||||
from tests.any_str import AnyStr
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
NEEDS_CONTEXTVARS = pytest.mark.skipif(
|
||||
sys.version_info < (3, 11),
|
||||
reason="Python 3.11+ is required for async contextvars support",
|
||||
)
|
||||
|
||||
|
||||
class ParentState(TypedDict):
|
||||
result: str
|
||||
|
||||
|
||||
# -- checkpointer=None (stateless) --
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateless_interrupt_resume_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None (the default) can
|
||||
still support interrupt/resume when invoked from inside a parent graph that
|
||||
has a checkpointer. This is the "stateless" pattern — the subgraph inherits
|
||||
the parent's checkpointer just enough to pause and resume, but does not
|
||||
retain any state across separate parent invocations. This pattern commonly
|
||||
appears when an agent is invoked from inside a tool used by another agent.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing.
|
||||
# Two nodes: "process" interrupts then echoes, "respond" returns "Done".
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
resp = await inner.ainvoke({"messages": [HumanMessage(content="apples")]})
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke hits the interrupt
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume completes the subgraph
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateless_state_resets_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None (the default) does
|
||||
not retain any message history between separate parent invocations. Each time
|
||||
the parent graph invokes the subgraph, it starts with a clean slate. This
|
||||
confirms the "stateless" behavior: even though the parent has a checkpointer,
|
||||
the subgraph state is not persisted across calls.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph: echoes "Processing: <input>"
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = await inner.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processing: tell me about apples"}
|
||||
|
||||
result2 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processing: tell me about bananas"}
|
||||
|
||||
# Both invocations produce fresh history — no memory of prior call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateless_state_resets_with_interrupt_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=None resets its state
|
||||
between parent invocations even when interrupt/resume is used. The subgraph
|
||||
is invoked twice from the parent, each time with an interrupt that must be
|
||||
resumed. After both invoke+resume cycles, each subgraph run should only
|
||||
contain its own messages — no bleed-over from the previous run.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = await inner.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke+resume cycle
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Second invoke+resume cycle
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Both invocations produce fresh history — no memory of prior call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
# -- checkpointer=False --
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_checkpointer_false_no_persistence_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=False gets no
|
||||
persistence at all, even when the parent graph has a checkpointer. Unlike
|
||||
the default (checkpointer=None) which inherits just enough from the parent
|
||||
to support interrupt/resume, checkpointer=False explicitly opts out of all
|
||||
checkpoint behavior. Each invocation starts completely fresh.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph with checkpointer=False
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processed: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile(checkpointer=False)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
topic = "apples" if call_count == 1 else "bananas"
|
||||
resp = await inner.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processed: tell me about apples"}
|
||||
|
||||
result2 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processed: tell me about bananas"}
|
||||
|
||||
# Both start fresh — no history from first call
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processed: tell me about apples",
|
||||
]
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about bananas",
|
||||
"Processed: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
# -- checkpointer=True (stateful) --
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateful_state_accumulates_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a subgraph compiled with checkpointer=True ("stateful")
|
||||
retains its message history across separate parent invocations. To enable
|
||||
this, the subgraph is wrapped in an outer graph compiled with
|
||||
checkpointer=True — this wrapper gives the inner subgraph its own persistent
|
||||
checkpoint namespace. After two parent calls, the second subgraph invocation
|
||||
should see messages from both the first and second calls.
|
||||
"""
|
||||
|
||||
# Build a simple echo subgraph
|
||||
def echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap the inner subgraph with checkpointer=True to enable stateful.
|
||||
# The wrapper graph gives the subgraph its own persistent checkpoint
|
||||
# namespace, keyed by the node name ("agent").
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = await wrapper.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Processing: tell me about apples"}
|
||||
|
||||
result2 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Processing: tell me about bananas"}
|
||||
|
||||
# First call: fresh history
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
]
|
||||
# Second call: retains messages from first call
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateful_state_accumulates_with_interrupt_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a stateful subgraph (checkpointer=True) retains its
|
||||
message history across parent invocations even when interrupt/resume is
|
||||
involved. The subgraph interrupts before echoing, then responds "Done".
|
||||
After two invoke+resume cycles, the second run should contain the full
|
||||
accumulated history from both calls.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap with checkpointer=True for stateful
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = await wrapper.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invoke+resume cycle
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# Second invoke+resume cycle
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
|
||||
# First call: fresh history
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
# Second call: retains messages from first call
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateful_interrupt_resume_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that a stateful subgraph (checkpointer=True) correctly
|
||||
supports interrupt/resume while also accumulating state. Each invoke+resume
|
||||
pair triggers the subgraph, and after the second pair completes we verify
|
||||
both the per-step invoke outputs and the accumulated message history. This
|
||||
exercises the full lifecycle: interrupt, resume, state accumulation.
|
||||
"""
|
||||
|
||||
# Build a subgraph that interrupts before echoing, then responds "Done"
|
||||
def process(state: MessagesState) -> dict:
|
||||
interrupt("continue?")
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Processing: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
def respond(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content="Done")]}
|
||||
|
||||
inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("process", process)
|
||||
.add_node("respond", respond)
|
||||
.add_edge(START, "process")
|
||||
.add_edge("process", "respond")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap with checkpointer=True for stateful
|
||||
wrapper = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("agent", inner)
|
||||
.add_edge(START, "agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
subgraph_messages: list[list[str]] = []
|
||||
topics = ["apples", "bananas"]
|
||||
|
||||
async def call_inner(state: ParentState) -> dict:
|
||||
topic = topics[len(subgraph_messages)]
|
||||
resp = await wrapper.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"tell me about {topic}")]}
|
||||
)
|
||||
subgraph_messages.append([m.text for m in resp["messages"]])
|
||||
return {"result": resp["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_inner", call_inner)
|
||||
.add_edge(START, "call_inner")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
# First invocation: hits interrupt
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume: completes first call
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
assert subgraph_messages[0] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
]
|
||||
|
||||
# Second invocation: hits interrupt, state accumulated from first call
|
||||
result = await parent.ainvoke({"result": ""}, config)
|
||||
assert result == {
|
||||
"result": "",
|
||||
"__interrupt__": [Interrupt(value="continue?", id=AnyStr())],
|
||||
}
|
||||
|
||||
# Resume: completes second call with accumulated state
|
||||
result = await parent.ainvoke(Command(resume=True), config)
|
||||
assert result == {"result": "Done"}
|
||||
assert subgraph_messages[1] == [
|
||||
"tell me about apples",
|
||||
"Processing: tell me about apples",
|
||||
"Done",
|
||||
"tell me about bananas",
|
||||
"Processing: tell me about bananas",
|
||||
"Done",
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
async def test_stateful_namespace_isolation_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Tests that two different stateful subgraphs (checkpointer=True)
|
||||
maintain completely independent state when they use different wrapper node
|
||||
names. A "fruit_agent" and "veggie_agent" are each wrapped in their own
|
||||
stateful graph. After two parent invocations, each agent should only
|
||||
see its own accumulated history with no cross-contamination between them.
|
||||
"""
|
||||
|
||||
# Build two simple echo subgraphs with different prefixes
|
||||
def fruit_echo(state: MessagesState) -> dict:
|
||||
return {"messages": [AIMessage(content=f"Fruit: {state['messages'][-1].text}")]}
|
||||
|
||||
def veggie_echo(state: MessagesState) -> dict:
|
||||
return {
|
||||
"messages": [AIMessage(content=f"Veggie: {state['messages'][-1].text}")]
|
||||
}
|
||||
|
||||
fruit_inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", fruit_echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
veggie_inner = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("echo", veggie_echo)
|
||||
.add_edge(START, "echo")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Wrap each with checkpointer=True, using different node names to get
|
||||
# independent checkpoint namespaces
|
||||
fruit = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("fruit_agent", fruit_inner)
|
||||
.add_edge(START, "fruit_agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
veggie = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("veggie_agent", veggie_inner)
|
||||
.add_edge(START, "veggie_agent")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
fruit_msgs: list[list[str]] = []
|
||||
veggie_msgs: list[list[str]] = []
|
||||
call_count = 0
|
||||
|
||||
async def call_both(state: ParentState) -> dict:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
suffix = "round 1" if call_count == 1 else "round 2"
|
||||
f = await fruit.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"cherries {suffix}")]}
|
||||
)
|
||||
v = await veggie.ainvoke(
|
||||
{"messages": [HumanMessage(content=f"broccoli {suffix}")]}
|
||||
)
|
||||
fruit_msgs.append([m.text for m in f["messages"]])
|
||||
veggie_msgs.append([m.text for m in v["messages"]])
|
||||
return {"result": f["messages"][-1].text}
|
||||
|
||||
parent = (
|
||||
StateGraph(ParentState)
|
||||
.add_node("call_both", call_both)
|
||||
.add_edge(START, "call_both")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": str(uuid4())}}
|
||||
|
||||
result1 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result1 == {"result": "Fruit: cherries round 1"}
|
||||
|
||||
result2 = await parent.ainvoke({"result": ""}, config)
|
||||
assert result2 == {"result": "Fruit: cherries round 2"}
|
||||
|
||||
# First call: each agent sees only its own history
|
||||
assert fruit_msgs[0] == ["cherries round 1", "Fruit: cherries round 1"]
|
||||
assert veggie_msgs[0] == ["broccoli round 1", "Veggie: broccoli round 1"]
|
||||
|
||||
# Second call: each accumulated independently — no cross-contamination
|
||||
assert fruit_msgs[1] == [
|
||||
"cherries round 1",
|
||||
"Fruit: cherries round 1",
|
||||
"cherries round 2",
|
||||
"Fruit: cherries round 2",
|
||||
]
|
||||
assert veggie_msgs[1] == [
|
||||
"broccoli round 1",
|
||||
"Veggie: broccoli round 1",
|
||||
"broccoli round 2",
|
||||
"Veggie: broccoli round 2",
|
||||
]
|
||||
Generated
+31
-30
@@ -1348,7 +1348,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.13"
|
||||
version = "1.2.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -1360,14 +1360,14 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/bb/c501ca60556c11ac80d1454bdcac63cb33583ce4e64fc4535ad5a7d5c6ba/langchain_core-1.2.13.tar.gz", hash = "sha256:d2773d0d0130a356378db9a858cfeef64c3d64bc03722f1d4d6c40eb46fdf01b", size = 831612, upload-time = "2026-02-15T07:45:57.014Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/a7/4c992456dae89a8704afec03e3c2a0149ccc5f29c1cbdd5f4aa77628e921/langchain_core-1.2.16.tar.gz", hash = "sha256:055a4bfe7d62f4ac45ed49fd759ee2e6bdd15abf998fbeea695fda5da2de6413", size = 835286, upload-time = "2026-02-25T16:27:30.551Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/ab/60fd69e5d55f67d422baefddaaca523c42cd7510ab6aeb17db6ae57fb107/langchain_core-1.2.13-py3-none-any.whl", hash = "sha256:b31823e28d3eff1e237096d0bd3bf80c6f9624eb471a9496dbfbd427779f8d82", size = 500485, upload-time = "2026-02-15T07:45:55.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/a1/57d5feaa11dc2ebb40f3bc3d7bf4294b6703e152e56edea9d4c622475a6a/langchain_core-1.2.16-py3-none-any.whl", hash = "sha256:2768add9aa97232a7712580f678e0ba045ee1036c71fe471355be0434fcb6e30", size = 502219, upload-time = "2026-02-25T16:27:29.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.8"
|
||||
version = "1.0.10rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1548,7 +1548,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "../checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1569,6 +1569,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -1607,7 +1608,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "psycopg", specifier = ">=3.2.0" },
|
||||
{ name = "psycopg-pool", specifier = ">=3.2.0" },
|
||||
]
|
||||
@@ -1735,7 +1736,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1812,7 +1813,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.25.2" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -3180,14 +3181,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.2.0"
|
||||
version = "7.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/32/6fac13a11e73e1bc67a2ae821a72bfe4c2d8c4c48f0267e4a952be0f1bae/redis-7.2.0.tar.gz", hash = "sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26", size = 4901247, upload-time = "2026-02-16T17:16:22.797Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/31/1476f206482dd9bc53fdbbe9f6fbd5e05d153f18e54667ce839df331f2e6/redis-7.2.1.tar.gz", hash = "sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5", size = 4906735, upload-time = "2026-02-25T20:05:18.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/cf/f6180b67f99688d83e15c84c5beda831d1d341e95872d224f87ccafafe61/redis-7.2.0-py3-none-any.whl", hash = "sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497", size = 394898, upload-time = "2026-02-16T17:16:20.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3388,27 +3389,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint test test-fast test_watch integration_tests spell_check spell_fix benchmark profile
|
||||
.PHONY: all format lint type test test-fast test_watch integration_tests spell_check spell_fix benchmark profile
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -50,6 +50,9 @@ lint lint_diff lint_package lint_tests:
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run mypy langgraph --cache-dir $(MYPY_CACHE)
|
||||
|
||||
type:
|
||||
mkdir -p $(MYPY_CACHE) && uv run mypy langgraph --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
uv run ruff check --fix $(PYTHON_FILES)
|
||||
@@ -72,6 +75,7 @@ help:
|
||||
@echo '-- LINTING --'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
@echo 'spell_check - run codespell on the project'
|
||||
@echo 'spell_fix - run codespell on the project and fix the errors'
|
||||
@echo '-- TESTS --'
|
||||
|
||||
@@ -922,7 +922,7 @@ class ToolNode(RunnableCallable):
|
||||
raise TypeError(msg)
|
||||
|
||||
# Inject state, store, and runtime right before invocation
|
||||
injected_call = self._inject_tool_args(call, request.runtime)
|
||||
injected_call = self._inject_tool_args(call, request.runtime, tool)
|
||||
call_args = {**injected_call, "type": "tool_call"}
|
||||
|
||||
try:
|
||||
@@ -1075,7 +1075,7 @@ class ToolNode(RunnableCallable):
|
||||
raise TypeError(msg)
|
||||
|
||||
# Inject state, store, and runtime right before invocation
|
||||
injected_call = self._inject_tool_args(call, request.runtime)
|
||||
injected_call = self._inject_tool_args(call, request.runtime, tool)
|
||||
call_args = {**injected_call, "type": "tool_call"}
|
||||
|
||||
try:
|
||||
@@ -1281,6 +1281,7 @@ class ToolNode(RunnableCallable):
|
||||
self,
|
||||
tool_call: ToolCall,
|
||||
tool_runtime: ToolRuntime,
|
||||
tool: BaseTool | None = None,
|
||||
) -> ToolCall:
|
||||
"""Inject graph state, store, and runtime into tool call arguments.
|
||||
|
||||
@@ -1299,6 +1300,9 @@ class ToolNode(RunnableCallable):
|
||||
Must contain 'name', 'args', 'id', and 'type' fields.
|
||||
tool_runtime: The ToolRuntime instance containing all runtime context
|
||||
(state, config, store, context, stream_writer) to inject into tools.
|
||||
tool: Optional tool instance. When provided, allows injection for
|
||||
dynamically registered tools that are not in self.tools_by_name
|
||||
(e.g., tools added via middleware's wrap_tool_call).
|
||||
|
||||
Returns:
|
||||
A new ToolCall dictionary with the same structure as the input but with
|
||||
@@ -1312,10 +1316,12 @@ class ToolNode(RunnableCallable):
|
||||
This method is called automatically during tool execution. It should not
|
||||
be called from outside the `ToolNode`.
|
||||
"""
|
||||
if tool_call["name"] not in self.tools_by_name:
|
||||
return tool_call
|
||||
|
||||
injected = self._injected_args.get(tool_call["name"])
|
||||
if not injected and tool is not None:
|
||||
# For dynamically registered tools (e.g., added via middleware's
|
||||
# wrap_tool_call), compute injected args on-the-fly since they
|
||||
# were not present during ToolNode initialization.
|
||||
injected = _get_all_injected_args(tool)
|
||||
if not injected:
|
||||
return tool_call
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -1902,3 +1902,109 @@ async def test_tool_node_tool_runtime_generic() -> None:
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content == "test_info"
|
||||
assert tool_message.tool_call_id == "call_1"
|
||||
|
||||
|
||||
def test_tool_node_inject_runtime_dynamic_tool_via_wrap_tool_call() -> None:
|
||||
"""Test that ToolRuntime is injected for dynamically registered tools.
|
||||
|
||||
Regression test for https://github.com/langchain-ai/langchain/issues/35305.
|
||||
When a tool is dynamically provided via wrap_tool_call (not registered at
|
||||
ToolNode init time), ToolRuntime should still be injected into the tool.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def static_tool(x: int) -> str:
|
||||
"""A static tool registered at init."""
|
||||
return f"static: {x}"
|
||||
|
||||
@dec_tool
|
||||
def dynamic_tool_with_runtime(x: int, runtime: ToolRuntime) -> str:
|
||||
"""A dynamic tool that needs ToolRuntime injection."""
|
||||
return f"dynamic: x={x}, tool_call_id={runtime.tool_call_id}"
|
||||
|
||||
def wrap_tool_call(request, execute):
|
||||
"""Middleware that swaps in a dynamic tool."""
|
||||
if request.tool_call["name"] == "dynamic_tool_with_runtime":
|
||||
# Override tool to the dynamic one (not registered at init)
|
||||
new_request = request.override(tool=dynamic_tool_with_runtime)
|
||||
return execute(new_request)
|
||||
return execute(request)
|
||||
|
||||
# ToolNode only knows about static_tool at init time
|
||||
tool_node = ToolNode(
|
||||
[static_tool],
|
||||
wrap_tool_call=wrap_tool_call,
|
||||
)
|
||||
|
||||
# Verify the dynamic tool is NOT in the tool node's registered tools
|
||||
assert "dynamic_tool_with_runtime" not in tool_node.tools_by_name
|
||||
|
||||
# Call the dynamic tool
|
||||
tool_call = {
|
||||
"name": "dynamic_tool_with_runtime",
|
||||
"args": {"x": 42},
|
||||
"id": "call_dynamic_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("", tool_calls=[tool_call])
|
||||
result = tool_node.invoke(
|
||||
{"messages": [msg]},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# ToolRuntime should be injected and the tool should execute successfully
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "dynamic: x=42, tool_call_id=call_dynamic_1"
|
||||
assert tool_message.tool_call_id == "call_dynamic_1"
|
||||
|
||||
|
||||
async def test_tool_node_inject_runtime_dynamic_tool_via_wrap_tool_call_async() -> None:
|
||||
"""Test that ToolRuntime is injected for dynamically registered tools (async).
|
||||
|
||||
Async version of the regression test for
|
||||
https://github.com/langchain-ai/langchain/issues/35305.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def static_tool(x: int) -> str:
|
||||
"""A static tool registered at init."""
|
||||
return f"static: {x}"
|
||||
|
||||
@dec_tool
|
||||
async def dynamic_tool_with_runtime(x: int, runtime: ToolRuntime) -> str:
|
||||
"""A dynamic async tool that needs ToolRuntime injection."""
|
||||
return f"dynamic: x={x}, tool_call_id={runtime.tool_call_id}"
|
||||
|
||||
async def awrap_tool_call(request, execute):
|
||||
"""Async middleware that swaps in a dynamic tool."""
|
||||
if request.tool_call["name"] == "dynamic_tool_with_runtime":
|
||||
new_request = request.override(tool=dynamic_tool_with_runtime)
|
||||
return await execute(new_request)
|
||||
return await execute(request)
|
||||
|
||||
# ToolNode only knows about static_tool at init time
|
||||
tool_node = ToolNode(
|
||||
[static_tool],
|
||||
awrap_tool_call=awrap_tool_call,
|
||||
)
|
||||
|
||||
# Verify the dynamic tool is NOT in the tool node's registered tools
|
||||
assert "dynamic_tool_with_runtime" not in tool_node.tools_by_name
|
||||
|
||||
# Call the dynamic tool
|
||||
tool_call = {
|
||||
"name": "dynamic_tool_with_runtime",
|
||||
"args": {"x": 42},
|
||||
"id": "call_dynamic_2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("", tool_calls=[tool_call])
|
||||
result = await tool_node.ainvoke(
|
||||
{"messages": [msg]},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# ToolRuntime should be injected and the tool should execute successfully
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "dynamic: x=42, tool_call_id=call_dynamic_2"
|
||||
assert tool_message.tool_call_id == "call_dynamic_2"
|
||||
|
||||
Generated
+6
-5
@@ -268,7 +268,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.8"
|
||||
version = "1.0.10rc1"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -352,7 +352,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "../checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -373,6 +373,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -411,7 +412,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "psycopg", specifier = ">=3.2.0" },
|
||||
{ name = "psycopg-pool", specifier = ">=3.2.0" },
|
||||
]
|
||||
@@ -489,7 +490,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -585,7 +586,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.25.2" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: lint format test
|
||||
.PHONY: lint type format test
|
||||
|
||||
test:
|
||||
uv run pytest tests
|
||||
@@ -19,6 +19,9 @@ lint lint_diff:
|
||||
[ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
|
||||
uv run ty check .
|
||||
|
||||
type:
|
||||
uv run ty check .
|
||||
|
||||
format format_diff:
|
||||
uv run ruff check --select I --fix $(PYTHON_FILES)
|
||||
uv run ruff format $(PYTHON_FILES)
|
||||
|
||||
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
|
||||
from langgraph_sdk.encryption import Encryption
|
||||
from langgraph_sdk.encryption.types import EncryptionContext
|
||||
|
||||
__version__ = "0.3.6"
|
||||
__version__ = "0.3.9"
|
||||
|
||||
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
|
||||
|
||||
@@ -290,7 +290,7 @@ class AssistantsClient:
|
||||
"""
|
||||
get_params = {"recurse": recurse}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
if namespace is not None:
|
||||
return await self.http.get(
|
||||
f"/assistants/{assistant_id}/subgraphs/{namespace}",
|
||||
@@ -425,9 +425,9 @@ class AssistantsClient:
|
||||
payload: dict[str, Any] = {}
|
||||
if graph_id:
|
||||
payload["graph_id"] = graph_id
|
||||
if config:
|
||||
if config is not None:
|
||||
payload["config"] = config
|
||||
if context:
|
||||
if context is not None:
|
||||
payload["context"] = context
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
|
||||
@@ -110,7 +110,7 @@ def get_client(
|
||||
if url is None:
|
||||
url = "http://api"
|
||||
if os.environ.get("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT") == "true":
|
||||
transport = get_asgi_transport()(app=None, root_path="/noauth")
|
||||
transport = get_asgi_transport()(app=None, root_path="/noauth") # type: ignore[invalid-argument-type]
|
||||
_registered_transports.append(transport)
|
||||
else:
|
||||
try:
|
||||
@@ -122,7 +122,7 @@ def get_client(
|
||||
"Failed to connect to in-process LangGraph server. Deferring configuration.",
|
||||
exc_info=True,
|
||||
)
|
||||
transport = get_asgi_transport()(app=None, root_path="/noauth")
|
||||
transport = get_asgi_transport()(app=None, root_path="/noauth") # type: ignore[invalid-argument-type]
|
||||
_registered_transports.append(transport)
|
||||
|
||||
if transport is None:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -14,11 +15,13 @@ from langgraph_sdk.schema import (
|
||||
Cron,
|
||||
CronSelectField,
|
||||
CronSortBy,
|
||||
Durability,
|
||||
Input,
|
||||
OnCompletionBehavior,
|
||||
QueryParamTypes,
|
||||
Run,
|
||||
SortOrder,
|
||||
StreamMode,
|
||||
)
|
||||
|
||||
|
||||
@@ -60,13 +63,17 @@ class CronClient:
|
||||
metadata: Mapping[str, Any] | None = None,
|
||||
config: Config | None = None,
|
||||
context: Context | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
checkpoint_during: bool | None = None, # deprecated
|
||||
interrupt_before: All | list[str] | None = None,
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -83,7 +90,7 @@ class CronClient:
|
||||
config: The configuration for the assistant.
|
||||
context: Static context to add to the assistant.
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption).
|
||||
checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
|
||||
interrupt_before: Nodes to interrupt immediately before they get executed.
|
||||
|
||||
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
||||
@@ -93,6 +100,13 @@ class CronClient:
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
enabled: Whether the cron job is enabled or not.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
"async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
|
||||
"sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
|
||||
"exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -118,6 +132,13 @@ class CronClient:
|
||||
)
|
||||
```
|
||||
"""
|
||||
if checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"schedule": schedule,
|
||||
"input": input,
|
||||
@@ -131,6 +152,10 @@ class CronClient:
|
||||
"webhook": webhook,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
if multitask_strategy:
|
||||
payload["multitask_strategy"] = multitask_strategy
|
||||
@@ -151,7 +176,7 @@ class CronClient:
|
||||
metadata: Mapping[str, Any] | None = None,
|
||||
config: Config | None = None,
|
||||
context: Context | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
checkpoint_during: bool | None = None, # deprecated
|
||||
interrupt_before: All | list[str] | None = None,
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
@@ -159,6 +184,10 @@ class CronClient:
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -174,7 +203,7 @@ class CronClient:
|
||||
config: The configuration for the assistant.
|
||||
context: Static context to add to the assistant.
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption).
|
||||
checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
|
||||
interrupt_before: Nodes to interrupt immediately before they get executed.
|
||||
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
||||
webhook: Webhook to call after LangGraph API call is done.
|
||||
@@ -186,6 +215,13 @@ class CronClient:
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
enabled: Whether the cron job is enabled or not.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
"async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
|
||||
"sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
|
||||
"exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -211,6 +247,13 @@ class CronClient:
|
||||
```
|
||||
|
||||
"""
|
||||
if checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"schedule": schedule,
|
||||
"input": input,
|
||||
@@ -225,6 +268,10 @@ class CronClient:
|
||||
"on_run_completed": on_run_completed,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
if multitask_strategy:
|
||||
payload["multitask_strategy"] = multitask_strategy
|
||||
@@ -277,6 +324,10 @@ class CronClient:
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
on_run_completed: OnCompletionBehavior | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Cron:
|
||||
@@ -299,6 +350,10 @@ class CronClient:
|
||||
after execution. 'keep' creates a new thread for each execution but does not
|
||||
clean them up.
|
||||
enabled: Enable or disable the cron job.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -329,6 +384,10 @@ class CronClient:
|
||||
"interrupt_after": interrupt_after,
|
||||
"on_run_completed": on_run_completed,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return await self.http.patch(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
||||
from typing import Any, overload
|
||||
@@ -507,11 +508,11 @@ class RunsClient:
|
||||
|
||||
async def create_batch(
|
||||
self,
|
||||
payloads: list[RunCreate],
|
||||
payloads: builtins.list[RunCreate],
|
||||
*,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Run]:
|
||||
) -> builtins.list[Run]:
|
||||
"""Create a batch of stateless background runs."""
|
||||
|
||||
def filter_payload(payload: RunCreate):
|
||||
@@ -547,7 +548,7 @@ class RunsClient:
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
) -> list[dict] | dict[str, Any]: ...
|
||||
) -> builtins.list[dict] | dict[str, Any]: ...
|
||||
|
||||
@overload
|
||||
async def wait(
|
||||
@@ -572,7 +573,7 @@ class RunsClient:
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
) -> list[dict] | dict[str, Any]: ...
|
||||
) -> builtins.list[dict] | dict[str, Any]: ...
|
||||
|
||||
async def wait(
|
||||
self,
|
||||
@@ -600,7 +601,7 @@ class RunsClient:
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
durability: Durability | None = None,
|
||||
) -> list[dict] | dict[str, Any]:
|
||||
) -> builtins.list[dict] | dict[str, Any]:
|
||||
"""Create a run, wait until it finishes and return the final state.
|
||||
|
||||
Args:
|
||||
@@ -751,10 +752,10 @@ class RunsClient:
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
status: RunStatus | None = None,
|
||||
select: list[RunSelectField] | None = None,
|
||||
select: builtins.list[RunSelectField] | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Run]:
|
||||
) -> builtins.list[Run]:
|
||||
"""List runs.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -138,7 +138,7 @@ class StoreClient:
|
||||
if refresh_ttl is not None:
|
||||
get_params["refresh_ttl"] = refresh_ttl
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
return await self.http.get("/store/items", params=get_params, headers=headers)
|
||||
|
||||
async def delete_item(
|
||||
|
||||
@@ -260,6 +260,7 @@ class ThreadsClient:
|
||||
sort_by: ThreadSortBy | None = None,
|
||||
sort_order: SortOrder | None = None,
|
||||
select: list[ThreadSelectField] | None = None,
|
||||
extract: dict[str, str] | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Thread]:
|
||||
@@ -275,6 +276,13 @@ class ThreadsClient:
|
||||
offset: Offset in threads table to start search from.
|
||||
sort_by: Sort by field.
|
||||
sort_order: Sort order.
|
||||
select: List of fields to include in the response.
|
||||
extract: Dictionary mapping aliases to JSONB paths to extract
|
||||
from thread data. Paths use dot notation for nested keys and
|
||||
bracket notation for array indices (e.g.,
|
||||
`{"last_msg": "values.messages[-1]"}`). Extracted values are
|
||||
returned in an `extracted` field on each thread. Maximum 10
|
||||
paths per request.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -312,6 +320,8 @@ class ThreadsClient:
|
||||
payload["sort_order"] = sort_order
|
||||
if select:
|
||||
payload["select"] = select
|
||||
if extract:
|
||||
payload["extract"] = extract
|
||||
return await self.http.post(
|
||||
"/threads/search",
|
||||
json=payload,
|
||||
@@ -543,7 +553,7 @@ class ThreadsClient:
|
||||
elif checkpoint_id:
|
||||
get_params = {"subgraphs": subgraphs}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
return await self.http.get(
|
||||
f"/threads/{thread_id}/state/{checkpoint_id}",
|
||||
params=get_params,
|
||||
@@ -552,7 +562,7 @@ class ThreadsClient:
|
||||
else:
|
||||
get_params = {"subgraphs": subgraphs}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
return await self.http.get(
|
||||
f"/threads/{thread_id}/state",
|
||||
params=get_params,
|
||||
|
||||
@@ -294,7 +294,7 @@ class SyncAssistantsClient:
|
||||
"""
|
||||
get_params = {"recurse": recurse}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
if namespace is not None:
|
||||
return self.http.get(
|
||||
f"/assistants/{assistant_id}/subgraphs/{namespace}",
|
||||
@@ -427,9 +427,9 @@ class SyncAssistantsClient:
|
||||
payload: dict[str, Any] = {}
|
||||
if graph_id:
|
||||
payload["graph_id"] = graph_id
|
||||
if config:
|
||||
if config is not None:
|
||||
payload["config"] = config
|
||||
if context:
|
||||
if context is not None:
|
||||
payload["context"] = context
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -14,11 +15,13 @@ from langgraph_sdk.schema import (
|
||||
Cron,
|
||||
CronSelectField,
|
||||
CronSortBy,
|
||||
Durability,
|
||||
Input,
|
||||
OnCompletionBehavior,
|
||||
QueryParamTypes,
|
||||
Run,
|
||||
SortOrder,
|
||||
StreamMode,
|
||||
)
|
||||
|
||||
|
||||
@@ -54,13 +57,17 @@ class SyncCronClient:
|
||||
metadata: Mapping[str, Any] | None = None,
|
||||
config: Config | None = None,
|
||||
context: Context | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
checkpoint_during: bool | None = None, # deprecated
|
||||
interrupt_before: All | list[str] | None = None,
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -77,7 +84,7 @@ class SyncCronClient:
|
||||
config: The configuration for the assistant.
|
||||
context: Static context to add to the assistant.
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption).
|
||||
checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
|
||||
interrupt_before: Nodes to interrupt immediately before they get executed.
|
||||
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
||||
webhook: Webhook to call after LangGraph API call is done.
|
||||
@@ -85,6 +92,13 @@ class SyncCronClient:
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
enabled: Whether the cron job is enabled. By default, it is considered enabled.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
"async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
|
||||
"sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
|
||||
"exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
|
||||
headers: Optional custom headers to include with the request.
|
||||
|
||||
Returns:
|
||||
@@ -109,6 +123,13 @@ class SyncCronClient:
|
||||
)
|
||||
```
|
||||
"""
|
||||
if checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"schedule": schedule,
|
||||
"input": input,
|
||||
@@ -123,6 +144,10 @@ class SyncCronClient:
|
||||
"multitask_strategy": multitask_strategy,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return self.http.post(
|
||||
@@ -141,7 +166,7 @@ class SyncCronClient:
|
||||
metadata: Mapping[str, Any] | None = None,
|
||||
config: Config | None = None,
|
||||
context: Context | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
checkpoint_during: bool | None = None, # deprecated
|
||||
interrupt_before: All | list[str] | None = None,
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
webhook: str | None = None,
|
||||
@@ -149,6 +174,10 @@ class SyncCronClient:
|
||||
multitask_strategy: str | None = None,
|
||||
end_time: datetime | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Run:
|
||||
@@ -164,7 +193,7 @@ class SyncCronClient:
|
||||
config: The configuration for the assistant.
|
||||
context: Static context to add to the assistant.
|
||||
!!! version-added "Added in version 0.6.0"
|
||||
checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption).
|
||||
checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
|
||||
interrupt_before: Nodes to interrupt immediately before they get executed.
|
||||
interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
|
||||
webhook: Webhook to call after LangGraph API call is done.
|
||||
@@ -176,6 +205,13 @@ class SyncCronClient:
|
||||
Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
|
||||
end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
|
||||
enabled: Whether the cron job is enabled. By default, it is considered enabled.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
"async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
|
||||
"sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
|
||||
"exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
|
||||
headers: Optional custom headers to include with the request.
|
||||
|
||||
Returns:
|
||||
@@ -201,6 +237,13 @@ class SyncCronClient:
|
||||
```
|
||||
|
||||
"""
|
||||
if checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"schedule": schedule,
|
||||
"input": input,
|
||||
@@ -216,6 +259,10 @@ class SyncCronClient:
|
||||
"multitask_strategy": multitask_strategy,
|
||||
"end_time": end_time.isoformat() if end_time else None,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return self.http.post(
|
||||
@@ -266,6 +313,10 @@ class SyncCronClient:
|
||||
interrupt_after: All | list[str] | None = None,
|
||||
on_run_completed: OnCompletionBehavior | None = None,
|
||||
enabled: bool | None = None,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> Cron:
|
||||
@@ -288,6 +339,10 @@ class SyncCronClient:
|
||||
after execution. 'keep' creates a new thread for each execution but does not
|
||||
clean them up.
|
||||
enabled: Enable or disable the cron job.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -318,6 +373,10 @@ class SyncCronClient:
|
||||
"interrupt_after": interrupt_after,
|
||||
"on_run_completed": on_run_completed,
|
||||
"enabled": enabled,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
return self.http.patch(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import warnings
|
||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||
from typing import Any, overload
|
||||
@@ -503,11 +504,11 @@ class SyncRunsClient:
|
||||
|
||||
def create_batch(
|
||||
self,
|
||||
payloads: list[RunCreate],
|
||||
payloads: builtins.list[RunCreate],
|
||||
*,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Run]:
|
||||
) -> builtins.list[Run]:
|
||||
"""Create a batch of stateless background runs."""
|
||||
|
||||
def filter_payload(payload: RunCreate):
|
||||
@@ -543,7 +544,7 @@ class SyncRunsClient:
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
) -> list[dict] | dict[str, Any]: ...
|
||||
) -> builtins.list[dict] | dict[str, Any]: ...
|
||||
|
||||
@overload
|
||||
def wait(
|
||||
@@ -568,7 +569,7 @@ class SyncRunsClient:
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
) -> list[dict] | dict[str, Any]: ...
|
||||
) -> builtins.list[dict] | dict[str, Any]: ...
|
||||
|
||||
def wait(
|
||||
self,
|
||||
@@ -596,7 +597,7 @@ class SyncRunsClient:
|
||||
params: QueryParamTypes | None = None,
|
||||
on_run_created: Callable[[RunCreateMetadata], None] | None = None,
|
||||
durability: Durability | None = None,
|
||||
) -> list[dict] | dict[str, Any]:
|
||||
) -> builtins.list[dict] | dict[str, Any]:
|
||||
"""Create a run, wait until it finishes and return the final state.
|
||||
|
||||
Args:
|
||||
@@ -740,10 +741,10 @@ class SyncRunsClient:
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
status: RunStatus | None = None,
|
||||
select: list[RunSelectField] | None = None,
|
||||
select: builtins.list[RunSelectField] | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Run]:
|
||||
) -> builtins.list[Run]:
|
||||
"""List runs.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -255,6 +255,7 @@ class SyncThreadsClient:
|
||||
sort_by: ThreadSortBy | None = None,
|
||||
sort_order: SortOrder | None = None,
|
||||
select: list[ThreadSelectField] | None = None,
|
||||
extract: dict[str, str] | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> list[Thread]:
|
||||
@@ -268,7 +269,17 @@ class SyncThreadsClient:
|
||||
Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
limit: Limit on number of threads to return.
|
||||
offset: Offset in threads table to start search from.
|
||||
sort_by: Sort by field.
|
||||
sort_order: Sort order.
|
||||
select: List of fields to include in the response.
|
||||
extract: Dictionary mapping aliases to JSONB paths to extract
|
||||
from thread data. Paths use dot notation for nested keys and
|
||||
bracket notation for array indices (e.g.,
|
||||
`{"last_msg": "values.messages[-1]"}`). Extracted values are
|
||||
returned in an `extracted` field on each thread. Maximum 10
|
||||
paths per request.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
Returns:
|
||||
List of the threads matching the search parameters.
|
||||
@@ -303,6 +314,8 @@ class SyncThreadsClient:
|
||||
payload["sort_order"] = sort_order
|
||||
if select:
|
||||
payload["select"] = select
|
||||
if extract:
|
||||
payload["extract"] = extract
|
||||
return self.http.post(
|
||||
"/threads/search", json=payload, headers=headers, params=params
|
||||
)
|
||||
@@ -530,7 +543,7 @@ class SyncThreadsClient:
|
||||
elif checkpoint_id:
|
||||
get_params = {"subgraphs": subgraphs}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
return self.http.get(
|
||||
f"/threads/{thread_id}/state/{checkpoint_id}",
|
||||
params=get_params,
|
||||
@@ -539,7 +552,7 @@ class SyncThreadsClient:
|
||||
else:
|
||||
get_params = {"subgraphs": subgraphs}
|
||||
if params:
|
||||
get_params = {**get_params, **params}
|
||||
get_params = {**get_params, **dict(params)}
|
||||
return self.http.get(
|
||||
f"/threads/{thread_id}/state",
|
||||
params=get_params,
|
||||
|
||||
@@ -46,34 +46,51 @@ class Auth:
|
||||
|
||||
my_auth = Auth()
|
||||
|
||||
async def verify_token(token: str) -> str:
|
||||
# Verify token and return user_id
|
||||
# This would typically be a call to your auth server
|
||||
return "user_id"
|
||||
|
||||
@auth.authenticate
|
||||
async def authenticate(authorization: str) -> str:
|
||||
# Verify token and return user_id
|
||||
result = await verify_token(authorization)
|
||||
if result != "user_id":
|
||||
@my_auth.authenticate
|
||||
async def authenticate(authorization: str) -> Auth.types.MinimalUserDict:
|
||||
user = await verify_token(authorization) # Your token verification logic
|
||||
if not user:
|
||||
raise Auth.exceptions.HTTPException(
|
||||
status_code=401, detail="Unauthorized"
|
||||
)
|
||||
return result
|
||||
return {
|
||||
"identity": user["id"],
|
||||
"permissions": user.get("permissions", []),
|
||||
}
|
||||
|
||||
# Global fallback handler
|
||||
@auth.on
|
||||
async def authorize_default(params: Auth.on.value):
|
||||
return False # Reject all requests (default behavior)
|
||||
# Default deny: reject all requests that don't have a specific handler
|
||||
@my_auth.on
|
||||
async def deny_all(ctx: Auth.types.AuthContext, value: Any) -> False:
|
||||
return False
|
||||
|
||||
@auth.on.threads.create
|
||||
async def authorize_thread_create(params: Auth.on.threads.create.value):
|
||||
# Allow the allowed user to create a thread
|
||||
assert params.get("metadata", {}).get("owner") == "allowed_user"
|
||||
# Allow users to create threads with their own identity as owner
|
||||
@my_auth.on.threads.create
|
||||
async def allow_thread_create(
|
||||
ctx: Auth.types.AuthContext, value: Auth.types.on.threads.create.value
|
||||
):
|
||||
metadata = value.setdefault("metadata", {})
|
||||
metadata["owner"] = ctx.user.identity
|
||||
|
||||
@auth.on.store
|
||||
async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on):
|
||||
assert ctx.user.identity in value["namespace"], "Not authorized"
|
||||
# Allow users to read and search their own threads
|
||||
@my_auth.on.threads.read
|
||||
async def allow_thread_read(
|
||||
ctx: Auth.types.AuthContext, value: Auth.types.on.threads.read.value
|
||||
) -> Auth.types.FilterType:
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
@my_auth.on.threads.search
|
||||
async def allow_thread_search(
|
||||
ctx: Auth.types.AuthContext, value: Auth.types.on.threads.search.value
|
||||
) -> Auth.types.FilterType:
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
# Scope all store operations to the user's namespace
|
||||
@my_auth.on.store
|
||||
async def scope_store(ctx: Auth.types.AuthContext, value: Auth.types.on.store.value):
|
||||
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
|
||||
if not namespace or namespace[0] != ctx.user.identity:
|
||||
namespace = (ctx.user.identity, *namespace)
|
||||
value["namespace"] = namespace
|
||||
```
|
||||
|
||||
???+ note "Request Processing Flow"
|
||||
@@ -132,30 +149,31 @@ class Auth:
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Global handler for all requests:
|
||||
Start by denying all requests by default, then add specific handlers
|
||||
to allow access:
|
||||
|
||||
```python
|
||||
# Default deny: reject all unhandled requests
|
||||
@auth.on
|
||||
async def reject_unhandled_requests(ctx: AuthContext, value: Any) -> None:
|
||||
print(f"Request to {ctx.path} by {ctx.user.identity}")
|
||||
async def deny_all(ctx: AuthContext, value: Any) -> False:
|
||||
return False
|
||||
```
|
||||
|
||||
Resource-specific handler. This would take precedence over the global handler
|
||||
Resource-specific handler. This takes precedence over the global handler
|
||||
for all actions on the `threads` resource:
|
||||
|
||||
|
||||
```python
|
||||
@auth.on.threads
|
||||
async def check_thread_access(ctx: AuthContext, value: Any) -> bool:
|
||||
# Allow access only to threads created by the user
|
||||
return value.get("created_by") == ctx.user.identity
|
||||
async def allow_thread_access(ctx: AuthContext, value: Any) -> Auth.types.FilterType:
|
||||
# Only allow access to threads owned by the user
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
|
||||
Resource and action specific handler:
|
||||
|
||||
```python
|
||||
@auth.on.threads.delete
|
||||
async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool:
|
||||
async def allow_admin_thread_deletion(ctx: AuthContext, value: Any) -> bool:
|
||||
# Only admins can delete threads
|
||||
return "admin" in ctx.user.permissions
|
||||
```
|
||||
@@ -163,20 +181,38 @@ class Auth:
|
||||
Multiple resources or actions:
|
||||
|
||||
```python
|
||||
@auth.on(resources=["threads", "runs"], actions=["create", "update"])
|
||||
async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool:
|
||||
# Implement rate limiting for write operations
|
||||
return await check_rate_limit(ctx.user.identity)
|
||||
@auth.on(resources=["threads", "assistants"], actions=["read", "search"])
|
||||
async def allow_reads(ctx: AuthContext, value: Any) -> Auth.types.FilterType:
|
||||
# Allow read/search access to resources owned by the user
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
|
||||
Auth for the `store` resource is a bit different since its structure is developer defined.
|
||||
You typically want to enforce user creds in the namespace.
|
||||
You typically want to scope store operations by rewriting the namespace to include the user's identity.
|
||||
The `value` dict is mutable — changes to `value["namespace"]` are used by the server for the actual operation.
|
||||
|
||||
```python
|
||||
@auth.on.store
|
||||
async def check_store_access(ctx: AuthContext, value: Auth.types.on) -> bool:
|
||||
# Assuming you structure your store like (store.aput((user_id, application_context), key, value))
|
||||
assert value["namespace"][0] == ctx.user.identity
|
||||
async def scope_store(ctx: AuthContext, value: Auth.types.on.store.value):
|
||||
# Allow store access but scope to user's namespace
|
||||
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
|
||||
if not namespace or namespace[0] != ctx.user.identity:
|
||||
namespace = (ctx.user.identity, *namespace)
|
||||
value["namespace"] = namespace
|
||||
```
|
||||
|
||||
You can also register handlers for specific store actions:
|
||||
|
||||
```python
|
||||
@auth.on.store.put
|
||||
async def allow_put(ctx: AuthContext, value: Auth.types.on.store.put.value):
|
||||
# Allow puts, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
|
||||
@auth.on.store.get
|
||||
async def allow_get(ctx: AuthContext, value: Auth.types.on.store.get.value):
|
||||
# Allow gets, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
```
|
||||
"""
|
||||
# These are accessed by the API. Changes to their names or types is
|
||||
@@ -483,9 +519,100 @@ class _CronsOn(
|
||||
Search = types.CronsSearch
|
||||
|
||||
|
||||
class _StoreActionOn(typing.Generic[T]):
|
||||
"""Decorator for registering a handler for a specific store action."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth: Auth,
|
||||
action: typing.Literal["put", "get", "search", "delete", "list_namespaces"],
|
||||
value: type[T],
|
||||
) -> None:
|
||||
self.auth = auth
|
||||
self.action = action
|
||||
self.value = value
|
||||
|
||||
def __call__(self, fn: _ActionHandler[T]) -> _ActionHandler[T]:
|
||||
_validate_handler(fn)
|
||||
_register_handler(self.auth, "store", self.action, fn)
|
||||
return fn
|
||||
|
||||
|
||||
class _StoreOn:
|
||||
def __init__(self, auth: Auth) -> None:
|
||||
self._auth = auth
|
||||
self.put = _StoreActionOn(auth, "put", types.StorePut)
|
||||
"""Register a handler for store put operations.
|
||||
|
||||
???+ example "Example"
|
||||
If using `@auth.on` to deny by default, register this handler to allow
|
||||
put operations (scoped to the user's namespace):
|
||||
|
||||
```python
|
||||
@auth.on.store.put
|
||||
async def allow_store_put(ctx: Auth.types.AuthContext, value: Auth.types.on.store.put.value):
|
||||
# Allow puts, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
```
|
||||
"""
|
||||
self.get = _StoreActionOn(auth, "get", types.StoreGet)
|
||||
"""Register a handler for store get operations.
|
||||
|
||||
???+ example "Example"
|
||||
If using `@auth.on` to deny by default, register this handler to allow
|
||||
get operations (scoped to the user's namespace):
|
||||
|
||||
```python
|
||||
@auth.on.store.get
|
||||
async def allow_store_get(ctx: Auth.types.AuthContext, value: Auth.types.on.store.get.value):
|
||||
# Allow gets, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
```
|
||||
"""
|
||||
self.search = _StoreActionOn(auth, "search", types.StoreSearch)
|
||||
"""Register a handler for store search operations.
|
||||
|
||||
???+ example "Example"
|
||||
If using `@auth.on` to deny by default, register this handler to allow
|
||||
search operations (scoped to the user's namespace):
|
||||
|
||||
```python
|
||||
@auth.on.store.search
|
||||
async def allow_store_search(ctx: Auth.types.AuthContext, value: Auth.types.on.store.search.value):
|
||||
# Allow searches, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
```
|
||||
"""
|
||||
self.delete = _StoreActionOn(auth, "delete", types.StoreDelete)
|
||||
"""Register a handler for store delete operations.
|
||||
|
||||
???+ example "Example"
|
||||
If using `@auth.on` to deny by default, register this handler to allow
|
||||
delete operations (scoped to the user's namespace):
|
||||
|
||||
```python
|
||||
@auth.on.store.delete
|
||||
async def allow_store_delete(ctx: Auth.types.AuthContext, value: Auth.types.on.store.delete.value):
|
||||
# Allow deletes, scoped to user's namespace
|
||||
value["namespace"] = (ctx.user.identity, *value["namespace"])
|
||||
```
|
||||
"""
|
||||
self.list_namespaces = _StoreActionOn(
|
||||
auth, "list_namespaces", types.StoreListNamespaces
|
||||
)
|
||||
"""Register a handler for store list_namespaces operations.
|
||||
|
||||
???+ example "Example"
|
||||
If using `@auth.on` to deny by default, register this handler to allow
|
||||
namespace listing (scoped to the user's prefix):
|
||||
|
||||
```python
|
||||
@auth.on.store.list_namespaces
|
||||
async def allow_list_ns(ctx: Auth.types.AuthContext, value: Auth.types.on.store.list_namespaces.value):
|
||||
# Allow listing, scoped to user's namespace prefix
|
||||
value["namespace"] = (ctx.user.identity,)
|
||||
```
|
||||
"""
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
@@ -572,40 +699,42 @@ class _On:
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Global handler for all requests:
|
||||
Start by denying all requests by default with a global handler,
|
||||
then add specific handlers to allow access:
|
||||
|
||||
```python
|
||||
# Default deny: reject all requests without a specific handler
|
||||
@auth.on
|
||||
async def log_all_requests(ctx: AuthContext, value: Any) -> None:
|
||||
print(f"Request to {ctx.path} by {ctx.user.identity}")
|
||||
return True
|
||||
async def deny_all(ctx: AuthContext, value: Any) -> False:
|
||||
return False
|
||||
```
|
||||
|
||||
Resource-specific handler:
|
||||
Resource-specific handler to allow access (takes precedence
|
||||
over the global deny handler):
|
||||
|
||||
```python
|
||||
@auth.on.threads
|
||||
async def check_thread_access(ctx: AuthContext, value: Any) -> bool:
|
||||
# Allow access only to threads created by the user
|
||||
return value.get("created_by") == ctx.user.identity
|
||||
async def allow_thread_access(ctx: AuthContext, value: Any) -> Auth.types.FilterType:
|
||||
# Allow access only to threads owned by the user
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
|
||||
Resource and action specific handler:
|
||||
|
||||
```python
|
||||
@auth.on.threads.delete
|
||||
async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool:
|
||||
# Only admins can delete threads
|
||||
return "admin" in ctx.user.permissions
|
||||
@auth.on.threads.create
|
||||
async def allow_thread_create(ctx: AuthContext, value: Any) -> None:
|
||||
# Allow thread creation, stamping the owner
|
||||
value.setdefault("metadata", {})["owner"] = ctx.user.identity
|
||||
```
|
||||
|
||||
Multiple resources or actions:
|
||||
|
||||
```python
|
||||
@auth.on(resources=["threads", "runs"], actions=["create", "update"])
|
||||
async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool:
|
||||
# Implement rate limiting for write operations
|
||||
return await check_rate_limit(ctx.user.identity)
|
||||
@auth.on(resources=["threads", "assistants"], actions=["read", "search"])
|
||||
async def allow_reads(ctx: AuthContext, value: Any) -> Auth.types.FilterType:
|
||||
# Allow read/search, scoped to user's resources
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
@@ -233,13 +233,20 @@ class StudioUser:
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Use `@auth.on` to deny by default, but allow Studio users through:
|
||||
|
||||
```python
|
||||
@auth.on
|
||||
async def allow_developers(ctx: Auth.types.AuthContext, value: Any) -> None:
|
||||
async def deny_all_except_studio(ctx: Auth.types.AuthContext, value: Any) -> bool:
|
||||
# Allow Studio users, deny everyone else by default
|
||||
if isinstance(ctx.user, Auth.types.StudioUser):
|
||||
return None
|
||||
...
|
||||
return True
|
||||
return False
|
||||
|
||||
# Then add specific handlers to allow access for non-Studio users
|
||||
@auth.on.threads
|
||||
async def allow_thread_access(ctx: Auth.types.AuthContext, value: Any) -> Auth.types.FilterType:
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -402,7 +409,7 @@ class AuthContext(BaseAuthContext):
|
||||
"list_namespaces",
|
||||
]
|
||||
"""The action being performed on the resource.
|
||||
|
||||
|
||||
Most resources support the following actions:
|
||||
- create: Create a new resource
|
||||
- read: Read information about a resource
|
||||
@@ -411,8 +418,10 @@ class AuthContext(BaseAuthContext):
|
||||
- search: Search for resources
|
||||
|
||||
The store supports the following actions:
|
||||
- put: Add or update a document in the store
|
||||
- get: Get a document from the store
|
||||
- put: Add or update an item in the store
|
||||
- get: Get an item from the store
|
||||
- search: Search for items within a namespace prefix
|
||||
- delete: Delete an item from the store
|
||||
- list_namespaces: List the namespaces in the store
|
||||
"""
|
||||
|
||||
@@ -851,20 +860,34 @@ class CronsSearch(typing.TypedDict, total=False):
|
||||
|
||||
|
||||
class StoreGet(typing.TypedDict):
|
||||
"""Operation to retrieve a specific item by its namespace and key."""
|
||||
"""Operation to retrieve a specific item by its namespace and key.
|
||||
|
||||
This dict is mutable — auth handlers can modify `namespace` to enforce
|
||||
access scoping (e.g., prepending the user's identity).
|
||||
"""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that uniquely identifies the item's location."""
|
||||
"""Hierarchical path that uniquely identifies the item's location.
|
||||
|
||||
Auth handlers can modify this to enforce per-user scoping.
|
||||
"""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its specific namespace."""
|
||||
|
||||
|
||||
class StoreSearch(typing.TypedDict):
|
||||
"""Operation to search for items within a specified namespace hierarchy."""
|
||||
"""Operation to search for items within a specified namespace hierarchy.
|
||||
|
||||
This dict is mutable — auth handlers can modify `namespace` to enforce
|
||||
access scoping (e.g., prepending the user's identity).
|
||||
"""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Prefix filter for defining the search scope."""
|
||||
"""Prefix filter for defining the search scope.
|
||||
|
||||
Auth handlers can modify this to enforce per-user scoping.
|
||||
"""
|
||||
|
||||
filter: dict[str, typing.Any] | None
|
||||
"""Key-value pairs for filtering results based on exact matches or comparison operators."""
|
||||
@@ -876,14 +899,22 @@ class StoreSearch(typing.TypedDict):
|
||||
"""Number of matching items to skip for pagination."""
|
||||
|
||||
query: str | None
|
||||
"""Naturalj language search query for semantic search capabilities."""
|
||||
"""Natural language search query for semantic search capabilities."""
|
||||
|
||||
|
||||
class StoreListNamespaces(typing.TypedDict):
|
||||
"""Operation to list and filter namespaces in the store."""
|
||||
"""Operation to list and filter namespaces in the store.
|
||||
|
||||
This dict is mutable — auth handlers can modify `namespace` (the prefix)
|
||||
to enforce access scoping (e.g., prepending the user's identity).
|
||||
"""
|
||||
|
||||
namespace: tuple[str, ...] | None
|
||||
"""Prefix filter namespaces."""
|
||||
"""Prefix filter for namespaces. Can be `None` if no prefix was provided.
|
||||
|
||||
Auth handlers can modify this to enforce per-user scoping. When `None`,
|
||||
handlers should set it to `(user_id,)` to scope listing to the user's namespaces.
|
||||
"""
|
||||
|
||||
suffix: tuple[str, ...] | None
|
||||
"""Optional conditions for filtering namespaces."""
|
||||
@@ -903,10 +934,17 @@ class StoreListNamespaces(typing.TypedDict):
|
||||
|
||||
|
||||
class StorePut(typing.TypedDict):
|
||||
"""Operation to store, update, or delete an item in the store."""
|
||||
"""Operation to store, update, or delete an item in the store.
|
||||
|
||||
This dict is mutable — auth handlers can modify `namespace` to enforce
|
||||
access scoping (e.g., prepending the user's identity).
|
||||
"""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that identifies the location of the item."""
|
||||
"""Hierarchical path that identifies the location of the item.
|
||||
|
||||
Auth handlers can modify this to enforce per-user scoping.
|
||||
"""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its namespace."""
|
||||
@@ -919,10 +957,17 @@ class StorePut(typing.TypedDict):
|
||||
|
||||
|
||||
class StoreDelete(typing.TypedDict):
|
||||
"""Operation to delete an item from the store."""
|
||||
"""Operation to delete an item from the store.
|
||||
|
||||
This dict is mutable — auth handlers can modify `namespace` to enforce
|
||||
access scoping (e.g., prepending the user's identity).
|
||||
"""
|
||||
|
||||
namespace: tuple[str, ...]
|
||||
"""Hierarchical path that uniquely identifies the item's location."""
|
||||
"""Hierarchical path that uniquely identifies the item's location.
|
||||
|
||||
Auth handlers can modify this to enforce per-user scoping.
|
||||
"""
|
||||
|
||||
key: str
|
||||
"""Unique identifier for the item within its specific namespace."""
|
||||
@@ -935,24 +980,27 @@ class on:
|
||||
and search operations across different resources (threads, assistants, crons).
|
||||
|
||||
???+ note "Usage"
|
||||
Start by denying all requests by default, then add handlers to allow access:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
auth = Auth()
|
||||
|
||||
# Default deny: reject all requests without a specific handler
|
||||
@auth.on
|
||||
def handle_all(params: Auth.on.value):
|
||||
raise Exception("Not authorized")
|
||||
async def deny_all(ctx: Auth.types.AuthContext, value: Auth.on.value):
|
||||
return False
|
||||
|
||||
# Allow thread creation, stamping the owner
|
||||
@auth.on.threads.create
|
||||
def handle_thread_create(params: Auth.on.threads.create.value):
|
||||
# Handle thread creation
|
||||
pass
|
||||
async def allow_thread_create(ctx: Auth.types.AuthContext, value: Auth.on.threads.create.value):
|
||||
value.setdefault("metadata", {})["owner"] = ctx.user.identity
|
||||
|
||||
# Allow assistant search, scoped to user's resources
|
||||
@auth.on.assistants.search
|
||||
def handle_assistant_search(params: Auth.on.assistants.search.value):
|
||||
# Handle assistant search
|
||||
pass
|
||||
async def allow_assistant_search(ctx: Auth.types.AuthContext, value: Auth.on.assistants.search.value):
|
||||
return {"owner": ctx.user.identity}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
Json = dict[str, Any] | None
|
||||
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
|
||||
@@ -146,7 +146,9 @@ AssistantSortBy = Literal[
|
||||
The field to sort by.
|
||||
"""
|
||||
|
||||
ThreadSortBy = Literal["thread_id", "status", "created_at", "updated_at"]
|
||||
ThreadSortBy = Literal[
|
||||
"thread_id", "status", "created_at", "updated_at", "state_updated_at"
|
||||
]
|
||||
"""
|
||||
The field to sort by.
|
||||
"""
|
||||
@@ -302,6 +304,8 @@ class Thread(TypedDict):
|
||||
"""The current state of the thread."""
|
||||
interrupts: dict[str, list[Interrupt]]
|
||||
"""Mapping of task ids to interrupts that were raised in that task."""
|
||||
extracted: NotRequired[dict[str, Any]]
|
||||
"""Extracted values from thread data. Only present when `extract` is used in search."""
|
||||
|
||||
|
||||
class ThreadTask(TypedDict):
|
||||
@@ -422,6 +426,14 @@ class CronUpdate(TypedDict, total=False):
|
||||
"""What to do with the thread after the run completes."""
|
||||
enabled: bool
|
||||
"""Enable or disable the cron job."""
|
||||
stream_mode: StreamMode | list[StreamMode]
|
||||
"""The stream mode(s) to use."""
|
||||
stream_subgraphs: bool
|
||||
"""Whether to stream output from subgraphs."""
|
||||
stream_resumable: bool
|
||||
"""Whether to persist the stream chunks in order to resume the stream later."""
|
||||
durability: Durability
|
||||
"""Durability level for the run. Must be one of 'sync', 'async', or 'exit'."""
|
||||
|
||||
|
||||
# Select field aliases for client-side typing of `select` parameters.
|
||||
|
||||
@@ -11,7 +11,7 @@ requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license-files = ['LICENSE']
|
||||
dependencies = ["httpx>=0.25.2", "orjson>=3.10.1"]
|
||||
dependencies = ["httpx>=0.25.2", "orjson>=3.11.5"]
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "langgraph_sdk/__init__.py"
|
||||
|
||||
@@ -19,7 +19,7 @@ class AsyncListByteStream(httpx.AsyncByteStream):
|
||||
self._chunks = list(chunks)
|
||||
self._exc = exc
|
||||
|
||||
async def __aiter__(self): # type: ignore[override]
|
||||
async def __aiter__(self):
|
||||
for chunk in self._chunks:
|
||||
yield chunk
|
||||
if self._exc is not None:
|
||||
@@ -34,7 +34,7 @@ class ListByteStream(httpx.ByteStream):
|
||||
self._chunks = list(chunks)
|
||||
self._exc = exc
|
||||
|
||||
def __iter__(self): # type: ignore[override]
|
||||
def __iter__(self):
|
||||
yield from self._chunks
|
||||
if self._exc is not None:
|
||||
raise self._exc
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_raise_for_status_typed_maps_exceptions_and_sets_status_code(
|
||||
with pytest.raises(exc_type) as ei:
|
||||
_raise_for_status_typed(r)
|
||||
|
||||
err = cast("APIStatusError", ei.value)
|
||||
err = ei.value
|
||||
assert err.status_code == status
|
||||
# response attribute should be present and match
|
||||
assert err.response.status_code == status
|
||||
@@ -113,7 +113,7 @@ def test_error_message_in_str_and_args() -> None:
|
||||
r = make_response(422, json_body={"message": "Validation failed"})
|
||||
with pytest.raises(UnprocessableEntityError) as ei:
|
||||
_raise_for_status_typed(r)
|
||||
err = cast("UnprocessableEntityError", ei.value)
|
||||
err = ei.value
|
||||
assert str(err) == "Validation failed"
|
||||
assert err.args == ("Validation failed",)
|
||||
assert err.message == "Validation failed"
|
||||
|
||||
Generated
+5
-4
@@ -265,7 +265,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.8"
|
||||
version = "1.0.10rc1"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -349,7 +349,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1rc3"
|
||||
source = { editable = "../checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -370,6 +370,7 @@ dev = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -396,7 +397,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -484,7 +485,7 @@ test = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.25.2" },
|
||||
{ name = "orjson", specifier = ">=3.10.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
Reference in New Issue
Block a user